EV/EBITDA Value, then momentum

I would like to present one of my projects based on the paper On the Performance of Cyclically Adjusted Valuation Measures by Gray and Vogel.

Most of the variables that one would change is right at the top.

First lets look at the fundamental screen that selects lowest positive 10 ev/ebitda ratios. Not bad.

Here is a summary of how the algorithm works.
Every x number of months, pick the stocks with a non-negative ev/ebitda and sort them from lowest to highest
Select only the lowest ev/ebitda
Then sort your resulting screen according to return of past days (I set it to 200) ignoring the last month
Pick the top momentum stock, and rescreen and rebalance every x number of months

602
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
#backtest note: No momentum
import pandas as pd
import numpy as np
import datetime
import math

def initialize(context):

#### Variables to change for your own liking #################
#the constant for portfolio turnover rate
context.holding_months = 1
#number of stocks to pass through the fundamental screener
context.num_screener = 10
#number of stocks in portfolio at any time
context.num_stock = 10
#number of days to "look back" if employing momentum. ie formation
context.formation_days = 200
#set False if you want the highest momentum, True if you want low
context.lowmom = False
#################################################################
#month counter for holding period logic.
context.month_count = context.holding_months

# Rebalance monthly on the first day of the month at market open
schedule_function(rebalance,
date_rule=date_rules.month_start(),
time_rule=time_rules.market_open())

def rebalance(context, data):
############temp code #####################
'''spy = symbol('SPY')
if data[spy].price < data[spy].mavg(120):
for stock in context.portfolio.positions:
order_target(stock, 0)
order_target_percent(symbol('TLT'), 1)
context.month_count += 1
print "moving towards TLT"
return'''

####################################
#This condition block is to skip every "holding_months"
if context.month_count >= context.holding_months:
context.month_count = 1
else:
context.month_count += 1
return

chosen_df = calc_return(context)

if context.num_stock < context.num_screener:
chosen_df = sort_return(chosen_df, context.lowmom)

chosen_df = chosen_df.iloc[:,:(context.num_stock-1)]

# Create weights for each stock
weight = 0.95/len(chosen_df.columns)
# Exit all positions before starting new ones
for stock in context.portfolio.positions:
if stock not in chosen_df:
order_target(stock, 0)

# Rebalance all stocks to target weights
for stock in chosen_df:
if weight != 0 and stock in data:
order_target_percent(stock, weight)

def sort_return(df, lowmom):
'''a cheap and quick way to sort columns according to index value. Sorts by descending order. Ie higher returns are first'''
df = df.T
df = df.sort(columns='return', ascending = lowmom)
df = df.T

return df

def calc_return(context):
price_history = history(bar_count=context.formation_days, frequency="1d", field='price')

temp = context.fundamental_df.copy()

for s in temp:
now = price_history[s].ix[-20]
old = price_history[s].ix[0]
pct_change = (now - old) / old
if np.isnan(pct_change):
temp = temp.drop(s,1)
else:
temp.loc['return', s] = pct_change#calculate percent change

return temp

"""
Called before the start of each trading day.
It updates our universe with the
securities and values found from fetch_fundamentals.
"""
#this code prevents query every day
if context.month_count != context.holding_months:
return

fundamental_df = get_fundamentals(
query(
# put your query in here by typing "fundamentals."
fundamentals.valuation_ratios.ev_to_ebitda,
fundamentals.asset_classification.morningstar_sector_code, fundamentals.valuation.enterprise_value, fundamentals.income_statement.ebit, fundamentals.income_statement.ebitda
)
.filter(fundamentals.valuation.market_cap > 2e9)
.filter(fundamentals.valuation_ratios.ev_to_ebitda > 0)
.filter(fundamentals.valuation.enterprise_value > 0)
.filter(fundamentals.asset_classification.morningstar_sector_code != 103)
.filter(fundamentals.asset_classification.morningstar_sector_code != 207)
.filter(fundamentals.valuation.shares_outstanding != None)
.order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
.limit(context.num_screener)
)

# Filter out only stocks that fits in criteria
context.stocks = [stock for stock in fundamental_df]
# Update context.fundamental_df with the securities that we need
context.fundamental_df = fundamental_df[context.stocks]

update_universe(context.fundamental_df.columns.values)

def create_weights(context, stocks):
"""
Takes in a list of securities and weights them all equally
"""
if len(stocks) == 0:
return 0
else:
# Buy only 0.9 of portfolio value to avoid borrowing
weight = .99/len(stocks)
return weight

#if the code gets to here, it means that there has been an error. I don't want my code to continue if there is a bull market and my screener doesn't have enough stocks that pass this filter
#def mom_filter(context, data):

def print_ev_ebitda(df):
fmean = df.mean(axis=1)
print fmean.loc['ev_to_ebitda']

def handle_data(context, data):
"""
Code logic to run during the trading day.
handle_data() gets called every bar.
"""
pass

There was a runtime error.
26 responses

Next, we select for low momentum stocks from an initial fundamental screen of 100 stocks.

Surprisingly, it's slightly better... probably because it selects stocks with higher beta?

602
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
#backtest note: low momentum
import pandas as pd
import numpy as np
import datetime
import math

def initialize(context):

#### Variables to change for your own liking #################
#the constant for portfolio turnover rate
context.holding_months = 1
#number of stocks to pass through the fundamental screener
context.num_screener = 100
#number of stocks in portfolio at any time
context.num_stock = 10
#number of days to "look back" if employing momentum. ie formation
context.formation_days = 200
#set False if you want the highest momentum, True if you want low
context.lowmom = True
#################################################################
#month counter for holding period logic.
context.month_count = context.holding_months

# Rebalance monthly on the first day of the month at market open
schedule_function(rebalance,
date_rule=date_rules.month_start(),
time_rule=time_rules.market_open())

def rebalance(context, data):
############temp code #####################
'''spy = symbol('SPY')
if data[spy].price < data[spy].mavg(120):
for stock in context.portfolio.positions:
order_target(stock, 0)
order_target_percent(symbol('TLT'), 1)
context.month_count += 1
print "moving towards TLT"
return'''

####################################
#This condition block is to skip every "holding_months"
if context.month_count >= context.holding_months:
context.month_count = 1
else:
context.month_count += 1
return

chosen_df = calc_return(context)

if context.num_stock < context.num_screener:
chosen_df = sort_return(chosen_df, context.lowmom)

chosen_df = chosen_df.iloc[:,:(context.num_stock-1)]

# Create weights for each stock
weight = 0.95/len(chosen_df.columns)
# Exit all positions before starting new ones
for stock in context.portfolio.positions:
if stock not in chosen_df:
order_target(stock, 0)

# Rebalance all stocks to target weights
for stock in chosen_df:
if weight != 0 and stock in data:
order_target_percent(stock, weight)

def sort_return(df, lowmom):
'''a cheap and quick way to sort columns according to index value. Sorts by descending order. Ie higher returns are first'''
df = df.T
df = df.sort(columns='return', ascending = lowmom)
df = df.T

return df

def calc_return(context):
price_history = history(bar_count=context.formation_days, frequency="1d", field='price')

temp = context.fundamental_df.copy()

for s in temp:
now = price_history[s].ix[-20]
old = price_history[s].ix[0]
pct_change = (now - old) / old
if np.isnan(pct_change):
temp = temp.drop(s,1)
else:
temp.loc['return', s] = pct_change#calculate percent change

return temp

"""
Called before the start of each trading day.
It updates our universe with the
securities and values found from fetch_fundamentals.
"""
#this code prevents query every day
if context.month_count != context.holding_months:
return

fundamental_df = get_fundamentals(
query(
# put your query in here by typing "fundamentals."
fundamentals.valuation_ratios.ev_to_ebitda,
fundamentals.asset_classification.morningstar_sector_code, fundamentals.valuation.enterprise_value, fundamentals.income_statement.ebit, fundamentals.income_statement.ebitda
)
.filter(fundamentals.valuation.market_cap > 2e9)
.filter(fundamentals.valuation_ratios.ev_to_ebitda > 0)
.filter(fundamentals.valuation.enterprise_value > 0)
.filter(fundamentals.asset_classification.morningstar_sector_code != 103)
.filter(fundamentals.asset_classification.morningstar_sector_code != 207)
.filter(fundamentals.valuation.shares_outstanding != None)
.order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
.limit(context.num_screener)
)

# Filter out only stocks that fits in criteria
context.stocks = [stock for stock in fundamental_df]
# Update context.fundamental_df with the securities that we need
context.fundamental_df = fundamental_df[context.stocks]

update_universe(context.fundamental_df.columns.values)

def create_weights(context, stocks):
"""
Takes in a list of securities and weights them all equally
"""
if len(stocks) == 0:
return 0
else:
# Buy only 0.9 of portfolio value to avoid borrowing
weight = .99/len(stocks)
return weight

#if the code gets to here, it means that there has been an error. I don't want my code to continue if there is a bull market and my screener doesn't have enough stocks that pass this filter
#def mom_filter(context, data):

def print_ev_ebitda(df):
fmean = df.mean(axis=1)
print fmean.loc['ev_to_ebitda']

def handle_data(context, data):
"""
Code logic to run during the trading day.
handle_data() gets called every bar.
"""
pass

There was a runtime error.

Lastly, we have the high momentum version

Returns are much better. A lot more volatility though, which is typical of momentum strategies.

You may be wondering why I chose such a short and odd interval from 2003-2007... That's because all 3 backtests stop working after 2011! From 2011-2015, neither value nor momentum outperforms the SP500!

Any thoughts?

602
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
#backtest note: high momentum
import pandas as pd
import numpy as np
import datetime
import math

def initialize(context):

#### Variables to change for your own liking #################
#the constant for portfolio turnover rate
context.holding_months = 1
#number of stocks to pass through the fundamental screener
context.num_screener = 100
#number of stocks in portfolio at any time
context.num_stock = 10
#number of days to "look back" if employing momentum. ie formation
context.formation_days = 200
#set False if you want the highest momentum, True if you want low
context.lowmom = False
#################################################################
#month counter for holding period logic.
context.month_count = context.holding_months

# Rebalance monthly on the first day of the month at market open
schedule_function(rebalance,
date_rule=date_rules.month_start(),
time_rule=time_rules.market_open())

def rebalance(context, data):
############temp code #####################
'''spy = symbol('SPY')
if data[spy].price < data[spy].mavg(120):
for stock in context.portfolio.positions:
order_target(stock, 0)
order_target_percent(symbol('TLT'), 1)
context.month_count += 1
print "moving towards TLT"
return'''

####################################
#This condition block is to skip every "holding_months"
if context.month_count >= context.holding_months:
context.month_count = 1
else:
context.month_count += 1
return

chosen_df = calc_return(context)

if context.num_stock < context.num_screener:
chosen_df = sort_return(chosen_df, context.lowmom)

chosen_df = chosen_df.iloc[:,:(context.num_stock-1)]

# Create weights for each stock
weight = 0.95/len(chosen_df.columns)
# Exit all positions before starting new ones
for stock in context.portfolio.positions:
if stock not in chosen_df:
order_target(stock, 0)

# Rebalance all stocks to target weights
for stock in chosen_df:
if weight != 0 and stock in data:
order_target_percent(stock, weight)

def sort_return(df, lowmom):
'''a cheap and quick way to sort columns according to index value. Sorts by descending order. Ie higher returns are first'''
df = df.T
df = df.sort(columns='return', ascending = lowmom)
df = df.T

return df

def calc_return(context):
price_history = history(bar_count=context.formation_days, frequency="1d", field='price')

temp = context.fundamental_df.copy()

for s in temp:
now = price_history[s].ix[-20]
old = price_history[s].ix[0]
pct_change = (now - old) / old
if np.isnan(pct_change):
temp = temp.drop(s,1)
else:
temp.loc['return', s] = pct_change#calculate percent change

return temp

"""
Called before the start of each trading day.
It updates our universe with the
securities and values found from fetch_fundamentals.
"""
#this code prevents query every day
if context.month_count != context.holding_months:
return

fundamental_df = get_fundamentals(
query(
# put your query in here by typing "fundamentals."
fundamentals.valuation_ratios.ev_to_ebitda,
fundamentals.asset_classification.morningstar_sector_code, fundamentals.valuation.enterprise_value, fundamentals.income_statement.ebit, fundamentals.income_statement.ebitda
)
.filter(fundamentals.valuation.market_cap > 2e9)
.filter(fundamentals.valuation_ratios.ev_to_ebitda > 0)
.filter(fundamentals.valuation.enterprise_value > 0)
.filter(fundamentals.asset_classification.morningstar_sector_code != 103)
.filter(fundamentals.asset_classification.morningstar_sector_code != 207)
.filter(fundamentals.valuation.shares_outstanding != None)
.order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
.limit(context.num_screener)
)

# Filter out only stocks that fits in criteria
context.stocks = [stock for stock in fundamental_df]
# Update context.fundamental_df with the securities that we need
context.fundamental_df = fundamental_df[context.stocks]

update_universe(context.fundamental_df.columns.values)

def create_weights(context, stocks):
"""
Takes in a list of securities and weights them all equally
"""
if len(stocks) == 0:
return 0
else:
# Buy only 0.9 of portfolio value to avoid borrowing
weight = .99/len(stocks)
return weight

#if the code gets to here, it means that there has been an error. I don't want my code to continue if there is a bull market and my screener doesn't have enough stocks that pass this filter
#def mom_filter(context, data):

def print_ev_ebitda(df):
fmean = df.mean(axis=1)
print fmean.loc['ev_to_ebitda']

def handle_data(context, data):
"""
Code logic to run during the trading day.
handle_data() gets called every bar.
"""
pass

There was a runtime error.

Here is a backtest with the same parameters as post #3 (high momentum), with the date range from 2003 to 2015 and I also turned on a moving average market exit towards long term treasuries (TLT).

602
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
#backtest note: high momentum
import pandas as pd
import numpy as np
import datetime
import math

def initialize(context):

#### Variables to change for your own liking #################
#the constant for portfolio turnover rate
context.holding_months = 1
#number of stocks to pass through the fundamental screener
context.num_screener = 100
#number of stocks in portfolio at any time
context.num_stock = 10
#number of days to "look back" if employing momentum. ie formation
context.formation_days = 200
#set False if you want the highest momentum, True if you want low
context.lowmom = False
#################################################################
#month counter for holding period logic.
context.month_count = context.holding_months

# Rebalance monthly on the first day of the month at market open
schedule_function(rebalance,
date_rule=date_rules.month_start(),
time_rule=time_rules.market_open())

def rebalance(context, data):
############temp code #####################
spy = symbol('SPY')
if data[spy].price < data[spy].mavg(120):
for stock in context.portfolio.positions:
order_target(stock, 0)
order_target_percent(symbol('TLT'), 1)
context.month_count += 1
print "moving towards TLT"
return

####################################
#This condition block is to skip every "holding_months"
if context.month_count >= context.holding_months:
context.month_count = 1
else:
context.month_count += 1
return

chosen_df = calc_return(context)

if context.num_stock < context.num_screener:
chosen_df = sort_return(chosen_df, context.lowmom)

chosen_df = chosen_df.iloc[:,:(context.num_stock-1)]

# Create weights for each stock
weight = 0.95/len(chosen_df.columns)
# Exit all positions before starting new ones
for stock in context.portfolio.positions:
if stock not in chosen_df:
order_target(stock, 0)

# Rebalance all stocks to target weights
for stock in chosen_df:
if weight != 0 and stock in data:
order_target_percent(stock, weight)

def sort_return(df, lowmom):
'''a cheap and quick way to sort columns according to index value. Sorts by descending order. Ie higher returns are first'''
df = df.T
df = df.sort(columns='return', ascending = lowmom)
df = df.T

return df

def calc_return(context):
price_history = history(bar_count=context.formation_days, frequency="1d", field='price')

temp = context.fundamental_df.copy()

for s in temp:
now = price_history[s].ix[-20]
old = price_history[s].ix[0]
pct_change = (now - old) / old
if np.isnan(pct_change):
temp = temp.drop(s,1)
else:
temp.loc['return', s] = pct_change#calculate percent change

return temp

"""
Called before the start of each trading day.
It updates our universe with the
securities and values found from fetch_fundamentals.
"""
#this code prevents query every day
if context.month_count != context.holding_months:
return

fundamental_df = get_fundamentals(
query(
# put your query in here by typing "fundamentals."
fundamentals.valuation_ratios.ev_to_ebitda,
fundamentals.asset_classification.morningstar_sector_code, fundamentals.valuation.enterprise_value, fundamentals.income_statement.ebit, fundamentals.income_statement.ebitda
)
.filter(fundamentals.valuation.market_cap > 2e9)
.filter(fundamentals.valuation_ratios.ev_to_ebitda > 0)
.filter(fundamentals.valuation.enterprise_value > 0)
.filter(fundamentals.asset_classification.morningstar_sector_code != 103)
.filter(fundamentals.asset_classification.morningstar_sector_code != 207)
.filter(fundamentals.valuation.shares_outstanding != None)
.order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
.limit(context.num_screener)
)

# Filter out only stocks that fits in criteria
context.stocks = [stock for stock in fundamental_df]
# Update context.fundamental_df with the securities that we need
context.fundamental_df = fundamental_df[context.stocks]

update_universe(context.fundamental_df.columns.values)

def create_weights(context, stocks):
"""
Takes in a list of securities and weights them all equally
"""
if len(stocks) == 0:
return 0
else:
# Buy only 0.9 of portfolio value to avoid borrowing
weight = .99/len(stocks)
return weight

#if the code gets to here, it means that there has been an error. I don't want my code to continue if there is a bull market and my screener doesn't have enough stocks that pass this filter
#def mom_filter(context, data):

def print_ev_ebitda(df):
fmean = df.mean(axis=1)
print fmean.loc['ev_to_ebitda']

def handle_data(context, data):
"""
Code logic to run during the trading day.
handle_data() gets called every bar.
"""
pass

There was a runtime error.

why it cannot have out performance backtest from 2010 to 2015

hahah that is an exercise left to the reader.

I have a feeling it is partly due to bad or inconsistent data. It doesn't explain why 2003-2007 returns are higher than 2011-2015 but it might explain the poor performance in the latter series.

I am currently writing code to excise stocks with bad data. For instance, stocks with negative ev or negative ebitda. EV/EBITDA as reported by morningstar does not equal EV divided by EBITDA as reported by morningstar on the same day. Strange huh.

Johnny,

I'll reach out to Morningstar to figure out how the EV/EBITDA ratio is calculated. I suspect the EBITDA values they use for the calculation might be some sort of difference in the value of EBITDA they're using (either a 12 month value or some other variant different from "the most recent reported quarter")

Josh

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.

Hi,

I heard back from Morningstar. The EV/EBITDA metric is calculated with a value of enterprise value that is updated daily and a trailing twelve month value of EBITDA which is updated quarterly. That trailing-twelve month value of EBITDA is not surfaced directly by our fundamental API today. The EBITDA value you have direct access to is the most recent quarter.

Hope that helps.

Thanks
Josh

Sorry, I don't quite understand. What do you mean by "That trailing-twelve month value of EBITDA is not surfaced directly by our fundamental API today"? By the most recent quarter, I assume that means most recent as of the datetime in simulation?

very often, the ebit and ebitda are nan, when ev/ebitda is a number. Did morningstar give any reason for that?

From what I remember there was a bug where ev-to-ebitda was not calculated daily but quarterly for a stock. If I plotted it to a graph u would find that it does vary with the daily changes in stock price. It should, if it was indeed being updated every day.

Trading IAU might be better than TLT, it tends to trend better in down times. Also it might be worth it to use a stop order.

Thanks Stian! I didn't think of using stop loss orders!

Found a paper that shows it works well with momentum
http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2407199

Here is the exact same code as post #3, but with a stop loss order of 10%. Once the stop loss is executed, the proceeds remain in cash for the rest of the month, just like in the paper outlined above.

In short, it doesn't work. In April 2004, it lost about 33%. It looks like at some point, the code shorts stocks. The stop order code is:
order_target_percent(stock, 0, style=StopOrder(data[stock].price*0.9))
Since it's target_percent 0%, why would it ever short stocks? It could be that an order remains unfilled due to poor liquidity, but the stock HUM is pretty liquid, and the offending shorts are 2 weeks after the portfolio is 0 (my strategy is long only).

I feel like the problem is due to my lack of understanding of how orders work in the API. I will keep working on this.

602
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
#backtest note: high momentum
import pandas as pd
import numpy as np
import datetime
import math

def initialize(context):

#### Variables to change for your own liking #################
#the constant for portfolio turnover rate
context.holding_months = 1
#number of stocks to pass through the fundamental screener
context.num_screener = 100
#number of stocks in portfolio at any time
context.num_stock = 10
#number of days to "look back" if employing momentum. ie formation
context.formation_days = 200
#set False if you want the highest momentum, True if you want low
context.lowmom = False
#################################################################
#month counter for holding period logic.
context.month_count = context.holding_months

# Rebalance monthly on the first day of the month at market open
schedule_function(rebalance,
date_rule=date_rules.month_start(),
time_rule=time_rules.market_open())

def rebalance(context, data):
############temp code #####################
spy = symbol('SPY')
if data[spy].price < data[spy].mavg(120):
for stock in context.portfolio.positions:
order_target(stock, 0)
order_target_percent(symbol('TLT'), 1)
context.month_count += 1
return

####################################
#This condition block is to skip every "holding_months"
if context.month_count >= context.holding_months:
context.month_count = 1
else:
context.month_count += 1
return

chosen_df = calc_return(context)

if context.num_stock < context.num_screener:
chosen_df = sort_return(chosen_df, context.lowmom)

chosen_df = chosen_df.iloc[:,:(context.num_stock-1)]

# Create weights for each stock
weight = 0.95/len(chosen_df.columns)
# Exit all positions before starting new ones
for stock in context.portfolio.positions:
if stock not in chosen_df:
order_target(stock, 0)

# Rebalance all stocks to target weights
for stock in chosen_df:
if weight != 0 and stock in data:
order_target_percent(stock, 0, style=StopOrder(data[stock].price*0.9))
order_target_percent(stock, weight)

def sort_return(df, lowmom):
'''a cheap and quick way to sort columns according to index value. Sorts by descending order. Ie higher returns are first'''
df = df.T
df = df.sort(columns='return', ascending = lowmom)
df = df.T

return df

def calc_return(context):
price_history = history(bar_count=context.formation_days, frequency="1d", field='price')

temp = context.fundamental_df.copy()

for s in temp:
now = price_history[s].ix[-20]
old = price_history[s].ix[0]
pct_change = (now - old) / old
if np.isnan(pct_change):
temp = temp.drop(s,1)
else:
temp.loc['return', s] = pct_change#calculate percent change

return temp

"""
Called before the start of each trading day.
It updates our universe with the
securities and values found from fetch_fundamentals.
"""
#this code prevents query every day
if context.month_count != context.holding_months:
return

fundamental_df = get_fundamentals(
query(
# put your query in here by typing "fundamentals."
fundamentals.valuation_ratios.ev_to_ebitda,
fundamentals.asset_classification.morningstar_sector_code, fundamentals.valuation.enterprise_value, fundamentals.income_statement.ebit, fundamentals.income_statement.ebitda
)
.filter(fundamentals.valuation.market_cap > 2e9)
.filter(fundamentals.valuation_ratios.ev_to_ebitda > 0)
.filter(fundamentals.valuation.enterprise_value > 0)
.filter(fundamentals.asset_classification.morningstar_sector_code != 103)
.filter(fundamentals.asset_classification.morningstar_sector_code != 207)
.filter(fundamentals.valuation.shares_outstanding != None)
.order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
.limit(context.num_screener)
)

# Filter out only stocks that fits in criteria
context.stocks = [stock for stock in fundamental_df]
# Update context.fundamental_df with the securities that we need
context.fundamental_df = fundamental_df[context.stocks]

update_universe(context.fundamental_df.columns.values)

def create_weights(context, stocks):
"""
Takes in a list of securities and weights them all equally
"""
if len(stocks) == 0:
return 0
else:
# Buy only 0.9 of portfolio value to avoid borrowing
weight = .99/len(stocks)
return weight

#if the code gets to here, it means that there has been an error. I don't want my code to continue if there is a bull market and my screener doesn't have enough stocks that pass this filter
#def mom_filter(context, data):

def print_ev_ebitda(df):
fmean = df.mean(axis=1)
print fmean.loc['ev_to_ebitda']

def handle_data(context, data):
"""
Code logic to run during the trading day.
handle_data() gets called every bar.
"""
pass

There was a runtime error.

I found a piece of code here:
https://www.quantopian.com/posts/how-to-cancel-all-open-orders. (many thanks to John Ricklefs)

I put the code cancel_everything() at the start of rebalance day. I'm not sure if this is an improvement. The drawdowns are roughly equivalent, and the Sharpe ratio is worse.

602
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
#backtest note: high momentum
import pandas as pd
import numpy as np
import datetime
import math

def initialize(context):

#### Variables to change for your own liking #################
#the constant for portfolio turnover rate
context.holding_months = 1
#number of stocks to pass through the fundamental screener
context.num_screener = 100
#number of stocks in portfolio at any time
context.num_stock = 10
#number of days to "look back" if employing momentum. ie formation
context.formation_days = 200
#set False if you want the highest momentum, True if you want low
context.lowmom = False
#################################################################
#month counter for holding period logic.
context.month_count = context.holding_months

# Rebalance monthly on the first day of the month at market open
schedule_function(rebalance,
date_rule=date_rules.month_start(),
time_rule=time_rules.market_open())

def rebalance(context, data):
############temp code #####################
spy = symbol('SPY')
if data[spy].price < data[spy].mavg(120):
for stock in context.portfolio.positions:
order_target(stock, 0)
order_target_percent(symbol('TLT'), 1)
context.month_count += 1
return

####################################
#This condition block is to skip every "holding_months"
if context.month_count >= context.holding_months:
context.month_count = 1
else:
context.month_count += 1
return

cancel_everything()

chosen_df = calc_return(context)

if context.num_stock < context.num_screener:
chosen_df = sort_return(chosen_df, context.lowmom)

chosen_df = chosen_df.iloc[:,:(context.num_stock-1)]

# Create weights for each stock
weight = 0.95/len(chosen_df.columns)
# Exit all positions before starting new ones
for stock in context.portfolio.positions:
if stock not in chosen_df:
order_target(stock, 0)

# Rebalance all stocks to target weights
for stock in chosen_df:
if weight != 0 and stock in data:
order_target_percent(stock, 0, style=StopOrder(data[stock].price*0.9))
order_target_percent(stock, weight)

def sort_return(df, lowmom):
'''a cheap and quick way to sort columns according to index value. Sorts by descending order. Ie higher returns are first'''
df = df.T
df = df.sort(columns='return', ascending = lowmom)
df = df.T

return df

def calc_return(context):
price_history = history(bar_count=context.formation_days, frequency="1d", field='price')

temp = context.fundamental_df.copy()

for s in temp:
now = price_history[s].ix[-20]
old = price_history[s].ix[0]
pct_change = (now - old) / old
if np.isnan(pct_change):
temp = temp.drop(s,1)
else:
temp.loc['return', s] = pct_change#calculate percent change

return temp

"""
Called before the start of each trading day.
It updates our universe with the
securities and values found from fetch_fundamentals.
"""
#this code prevents query every day
if context.month_count != context.holding_months:
return

fundamental_df = get_fundamentals(
query(
# put your query in here by typing "fundamentals."
fundamentals.valuation_ratios.ev_to_ebitda,
fundamentals.asset_classification.morningstar_sector_code, fundamentals.valuation.enterprise_value, fundamentals.income_statement.ebit, fundamentals.income_statement.ebitda
)
.filter(fundamentals.valuation.market_cap > 2e9)
.filter(fundamentals.valuation_ratios.ev_to_ebitda > 0)
.filter(fundamentals.valuation.enterprise_value > 0)
.filter(fundamentals.asset_classification.morningstar_sector_code != 103)
.filter(fundamentals.asset_classification.morningstar_sector_code != 207)
.filter(fundamentals.valuation.shares_outstanding != None)
.order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
.limit(context.num_screener)
)

# Filter out only stocks that fits in criteria
context.stocks = [stock for stock in fundamental_df]
# Update context.fundamental_df with the securities that we need
context.fundamental_df = fundamental_df[context.stocks]

update_universe(context.fundamental_df.columns.values)

def create_weights(context, stocks):
"""
Takes in a list of securities and weights them all equally
"""
if len(stocks) == 0:
return 0
else:
# Buy only 0.9 of portfolio value to avoid borrowing
weight = .99/len(stocks)
return weight

#if the code gets to here, it means that there has been an error. I don't want my code to continue if there is a bull market and my screener doesn't have enough stocks that pass this filter
#def mom_filter(context, data):

def print_ev_ebitda(df):
fmean = df.mean(axis=1)
print fmean.loc['ev_to_ebitda']

def cancel_everything():
"""
Cancels all open orders for all sids.
"""
all_open_orders = get_open_orders()
if all_open_orders:
for security, oo_for_sid in all_open_orders.iteritems():
for order_obj in oo_for_sid:
log.info("%s: Cancelling order for %s of %s created on %s" %
(get_datetime(), order_obj.amount,
security.symbol, order_obj.created))
cancel_order(order_obj)

def handle_data(context, data):
"""
Code logic to run during the trading day.
handle_data() gets called every bar.
"""
pass

There was a runtime error.

Amazing work Johnny , thanks for sharing this. I'm working a momentum strategy using country ETFs, will publish soon .

Thanks Lionel! Country ETF using momentum was going to be my next project. Did you watch the presentation by Meb Faber in Quantcom too?

I look forward to see what you came up with.

Thanks for sharing Johnny.
I see that you are filtering out Business Services and Media sectors when screening stocks. Any particular reason in doing so?

If I did, it was unintentional.
Based on this code:
context.sector_mappings = {101.0: "Basic Materials",
102.0: "Consumer Cyclical",
103.0: "Financial Services",
104.0: "Real Estate",
205.0: "Consumer Defensive",
206.0: "Healthcare",
207.0: "Utilites",
308.0: "Communication Services",
309.0: "Energy",
310.0: "Industrials",
311.0: "Technology"}

The ev/ebitda ratio works better than p/e ratio due to the fact that it allows for a fair comparison between companies. In greenblatt's book, he excludes finances and utilities because their bookkeeping is complex and hence their ev/ebitda cannot directly be compared in a simple mechanical way to companies outside their sector.

To be honest, I don't know any other details of why this should be.

Maybe someone more knowledgeable can chime and in explain it in greater detail

        .filter(fundamentals.asset_classification.morningstar_sector_code != 103)
.filter(fundamentals.asset_classification.morningstar_sector_code != 207)


It's a different table than the one I found on Morningstars' web site.
Then the selection is excluding Financial services and Utilities.

Could you please link me to the morningstar web site that has a different table?

Morningstar Global Equity Classification Structure

The sector classification is shown on page 2, the document is from 2009 though.
Perhaps the classification changed.

This is the reason why I love the community. I wouldn't have thought to doublecheck the accuracy of my morningstar sector codes!

I did a quick marketcap sort of 103. According to my references, it should be finances, but according to your link it should be media.

It returns such names as AIG, BRK, ING etc... pretty big finance firms.

207 should be utilities according to my reference but business services in yours. A quick marketcap sort returns DYN, EON, DUK, SO etc..

From my search, I'm pretty confident that my list is most accurate for the purpose of the Quantopian API. Again, thanks for the diligence.

There was a problem a few months ago that I reported about ev_to_ebitda value not being calculated every day.
I plotted the ev_to_ebitda value for stocks and they show updates once quarter and then flattening out till the next quarter.

And some one from quantopian confirm this was the case.

Is this still the case? Has this been fixed?
that would affect this algorithm

Saravanan: I have an unpublished piece of code that manually re-calculates based on the reported ebit and ev. It performs quite a bit better, so I suspect that this is still the case.

Sarvi,
For data prior to May 2014, metrics were only provided to us with a monthly frequency. So even metrics that change daily (like ev_to_ebitda) are only updated monthly in our data set in the timeframe 2002 - May 2014. This is an unfortunate fact of life for us with respect to the Morningstar data set. We deliver as much data as they've provided to us.

Subsequent to May 2014, we have daily updates for our metrics. This magic date in May 2014 is when we began our relationship with Morningstar. We subscribe to Morningstar's daily updates so this (and other metrics like market cap) update daily in our data set from May 2014 - present.

Unfortunately, I've found no way to fix this with what we get from Morningstar today.

I just wanted to add some information links related to this topic that I found useful. This post was the first time I had heard of the "Enterprise Multiple" ratio, so this might be useful for others:

The Single-Best Metric: EV/EBITDA

A Superior Metric for Value Investors

Get_fundamentals method is no longer working, so I updated the code accordingly. At first glance it provides a fantastic return (and indeed it does for the backtest period), but the algorithm underperforms this decade. It makes sense given that the performance of value factor has been progressively worsening for close to a decade. Sorting by high EV/EBITDA actually seems to give a slight edge.

110
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
#backtest note: No momentum
import pandas as pd
import numpy as np
import datetime
import math
from quantopian.pipeline.data import Fundamentals
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.algorithm import attach_pipeline, pipeline_output

def initialize(context):
#### Variables to change for your own liking #################
#the constant for portfolio turnover rate
context.holding_months = 1
#number of stocks to pass through the fundamental screener
context.num_screener = 100
#number of stocks in portfolio at any time
context.num_stock = 10
#number of days to "look back" if employing momentum. ie formation
context.formation_days = 200
#set False if you want the highest momentum, True if you want low
context.lowmom = False
#################################################################
#month counter for holding period logic.
context.month_count = context.holding_months
# Rebalance monthly on the first day of the month at market open
schedule_function(rebalance,
date_rule=date_rules.month_start(),
time_rule=time_rules.market_open())
attach_pipeline(make_pipeline(), 'pipe')

def make_pipeline():

filter_sectors = ((Fundamentals.morningstar_sector_code.latest != 103) &
(Fundamentals.morningstar_sector_code.latest != 207) #&
#(Fundamentals.morningstar_sector_code.latest != 206) &
#(Fundamentals.morningstar_sector_code.latest != 309) &
#(Fundamentals.morningstar_industry_code.latest != 20533080) &
#(Fundamentals.morningstar_industry_code.latest != 10217033) &
#(Fundamentals.morningstar_industry_group_code != 10106) &
#(Fundamentals.morningstar_industry_group_code != 10104)
)

filter_market_cap = (Fundamentals.market_cap.latest > 2e9) #& (filter_sectors)
filter_ev_to_ebitda = (Fundamentals.ev_to_ebitda.latest > 0) & (filter_market_cap)
filter_enterprise_value = (Fundamentals.enterprise_value.latest > 0) & (filter_ev_to_ebitda)
filter_shares_outstanding = (Fundamentals.shares_outstanding.latest.notnull()) & (filter_enterprise_value)
filter_ev_to_ebitda_rank = filter_ev_to_ebitda_rank.bottom(100)

pipe = Pipeline(columns = {'ev_to_ebitda': Fundamentals.ev_to_ebitda.latest,
'rank': Fundamentals.ev_to_ebitda.latest.rank(ascending = True),
'sector': Fundamentals.morningstar_sector_code.latest,
},screen = filter_ev_to_ebitda_rank
)
return pipe

def rebalance(context, data):
############temp code #####################
spy = symbol('SPY')
if data[spy].price < data[spy].mavg(120):
for stock in context.portfolio.positions:
order_target(stock, 0)
order_target_percent(symbol('TLT'), 1)
context.month_count += 1
print "moving towards TLT"
return

####################################
#This condition block is to skip every "holding_months"
if context.month_count >= context.holding_months:
context.month_count = 1
else:
context.month_count += 1
return

chosen_df = calc_return(context, data)

#if context.num_stock < context.num_screener:
chosen_df = sort_return(chosen_df, context.lowmom)
chosen_df = chosen_df.iloc[:context.num_stock]

# Cs for each stock
weight = 0.99/len(chosen_df)
# Exit all positions before starting new ones
for stock in context.portfolio.positions:
if stock not in chosen_df.index:
order_target(stock, 0)

# Rebalance all stocks to target weights
for stock in chosen_df.index:
if weight != 0 and data.can_trade(stock):
order_target_percent(stock, weight)

def sort_return(df, lowmom):
'''a cheap and quick way to sort columns according to index value. Sorts by descending order. Ie higher returns are first'''
return df.sort(columns='return', ascending = lowmom)

def calc_return(context, data):
price_history = data.history(context.fundamentals.index,
fields = 'price',
bar_count=context.formation_days,
frequency="1d")
temp = context.fundamentals.copy()

for s in context.fundamentals.index:
print(s)
now = price_history[s].ix[-20] # CHANGE THIS TO -1
old = price_history[s].ix[0]
pct_change = (now - old) / old
if np.isnan(pct_change):
temp = temp.drop(s,0)
else:
temp.loc[s,'return'] = pct_change#calculate percent change

context.stocks = temp.index
return temp

"""
Called before the start of each trading day.
It updates our universe with the
securities and values found from fetch_fundamentals.
"""
#this code prevents query every day
if context.month_count != context.holding_months:
return

# Filter out only stocks that fits in criteria
context.fundamentals = pipeline_output('pipe')
context.stocks = context.fundamentals.index
#print context.fundamentals


There was a runtime error.