Introduction
Beta is a statistical measure of a stock's volatility in relation to the market. Stock analysts use this measure to get a sense of stocks' risk profiles. It is also a key component of the capital asset pricing model (CAPM), A stock's price variability is essential to consider when assessing risk. It represents the co-movement instead of the volatility. Therefore, it is possible for a stock to have zero beta and higher volatility than the market.
In the real world, some investors are prohibited from using leverage and other investors’ leverage is limited by margin requirements. Therefore, their only way to achieve higher returns is to buy more risky stocks, which would cause the overvaluation of higher-beta stocks. This behavior suggests that high-beta (risky) stocks should deliver lower risk-adjusted returns than low-beta stocks. In this algorithm, we'll use the leverage to explore the inefficiency of the beta factor.
Method
The investment universe consists of all stocks in Nasdaq and NYSE. We use the Wilshire 5000 Total Market Index which covers all stocks actively traded in the United States.
def initialize(self):
self.add_universe(self.coarse_selection_function)
# Add Wilshire 5000 Total Market Index data from Dropbox
self.price5000 = self.add_data(Fred, Fred.wilshire.price5000, Resolution.DAILY).symbol
We set up a automatic ROC indicator and a 1-year rolling window to hold its daily return data for a year. We warm them up by historical data.
# Setup a RollingWindow to hold market return
self.market_return = RollingWindow[float](252)
# Use a ROC indicator to convert market price index into return, and save it to the RollingWindow
self.roc = self.ROC(self.price5000, 1)
self.roc.updated += lambda sender, updated: self.market_return.add(updated.value)
# Warm up
hist = self.history(self.price5000, 253, Resolution.DAILY)
for point in hist.itertuples():
self.roc.update(point.index[1], point.value)
The formula for calculating beta is the covariance of the return of an asset with the return of the market divided by the variance of the return of the market over a certain period.
\[\beta_i=\frac{cov(R_i,R_{m})}{Var(R_m)}\]
We choose the 1-year rolling window as the period in the beta calculation. We created the SymbolData
class to update the rolling window of return
and the calculation of beta.
class SymbolData:
def __init__(self, symbol):
self.symbol = symbol
self.last_price = 0
self.returns = RollingWindow[float](252)
self.roc = RateOfChange(1)
self.roc.updated += lambda sender, updated: self.returns.add(updated.value)
def update(self, time, price):
if price != 0:
self.last_price = price
self.roc.update(time, price)
def is_ready(self):
return self.roc.is_ready and self.returns.is_ready
def beta(self, market_ret):
asset_return = np.array(list(self.returns), dtype=np.float32)
market_return = np.array(list(market_ret), dtype=np.float32)
return np.cov(asset_return, market_return)[0][1]/np.var(market_return)
In the CoarseSelectionFunction, we filter the stocks which price is lower than five as they are not active in the market. When the return rolling window is ready, stocks are then ranked in ascending order on the basis of their estimated beta. The algorithm goes long on five stocks at the bottom beta list and short on five stocks at the top beta list.
def initialize(self):
self.data = {}
self.monthly_rebalance = False
self.long = None
self.short = None
def coarse_selection_function(self, coarse):
for c in coarse:
if c.symbol not in self.data:
self.data[c.symbol] = SymbolData(c.symbol)
self.data[c.symbol].update(c.end_time, c.adjusted_price)
if self.monthly_rebalance:
filtered_data = {symbol: data for symbol, data in self.data.items() if data.last_price > 5 and data.is_ready()}
if len(filtered_data) > 10:
# sort the dictionary in ascending order by beta value
sorted_beta = sorted(filtered_data, key = lambda x: filtered_data[x].beta(self.market_return))
self.long = sorted_beta[:5]
self.short = sorted_beta[-5:]
return self.long + self.short
else:
self.monthly_rebalance = False
return []
else:
return []
In each portfolio, securities are weighted by the ranked betas. Lower-beta stocks have larger weights in the low-beta portfolio and higher-beta stocks have larger weights in the high-beta portfolio. The portfolios are rebalanced every calendar month.
def on_data(self, data):
if not self.monthly_rebalance: return
# Liquidate symbols not in the universe anymore
for symbol in self.portfolio.keys:
if self.portfolio[symbol].invested and symbol not in self.long + self.short:
self.liquidate(symbol)
if self.long is None or self.short is None: return
long_scale_factor = 0.5/sum(range(1,len(self.long)+1))
for rank, symbol in enumerate(self.long):
self.set_holdings(symbol, (len(self.long)-rank+1)*long_scale_factor)
short_scale_factor = 0.5/sum(range(1,len(self.long)+1))
for rank, symbol in enumerate(self.short):
self.set_holdings(symbol, -(rank+1)*short_scale_factor)
self.monthly_rebalance = False
self.long = None
self.short = None
Derek Melchin
See the attached backtest for an updated version of the algorithm with the following changes:
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 QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect 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. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
Jing Wu
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 QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect 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. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
To unlock posting to the community forums please complete at least 30% of Boot Camp.
You can continue your Boot Camp training progress from the terminal. We hope to see you in the community soon!