Introduction
Some investors are prohibited from using leverage and other investors’ leverage is limited by margin requirements. Their only way to achieve higher returns is to buy more risky stocks which makes these assets more expensive. High-beta and risky assets should therefore deliver lower risk-adjusted returns than low-beta assets. Investors could exploit this inefficiency by using ETFs. This algorithm is going to explore this phenomenon.
Method
The implementation of this algorithm uses the algorithm framework. The algorithm picks 35 country indexes ETFs as the trading universe. As the symbols in the universe don't change over time, we use the ManualUniverseSelectionModel to subscribe the daily data for those symbols.
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. 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)}\]
def beta(self, asset_return, market_return):
asset_return = np.array(asset_return, dtype=np.float32)
market_return = np.array(market_return, dtype=np.float32)
return np.cov(asset_return, market_return)[0][1]/np.var(market_return)
We use S&P500 ETF as the market measure. The beta for each country is calculated with respect to the SPY using a 1-year rolling window. The SymbolData
class saves the one-year rolling window price data.
class SymbolData:
def __init__(self, symbol):
self.symbol = symbol
self.price = deque(maxlen=253)
self.assets
is a dictionary to save the price series for each country ETF. The key is the ETF symbol.
For new symbols added to the algorithm, we request the one-year history data to initialize the price series.
def on_securities_changed(self, algorithm, changes):
for added in changes.added_securities:
if added.symbol.value == "SPY":
self.market_price = deque(maxlen=253)
hist__s_p_y = algorithm.history(["SPY"], 500, Resolution.DAILY)
for i in hist__s_p_y.loc["SPY"].itertuples():
self.market_price.append(i.close)
if added not in self.assets and added.symbol.value != "SPY":
hist = algorithm.history([added.symbol.value], 500, Resolution.DAILY)
if not hist.empty:
self.assets[added.symbol] = SymbolData(added)
for i in hist.loc[added.symbol.value].itertuples():
self.assets[added.symbol].price.append(i.close)
for removed in changes.removed_securities:
self.assets.pop(removed.symbol)
In the Alpha model, Update(self, algorithm, data)
method updates this model with the latest data from the algorithm.
This method is called each time the algorithm receives data for subscribed securities. In this method, we update the price series with new trade bars.
The price series is converted to return series. We plug the market return and the country ETF return into the beta formula.
ETFs are then ranked in ascending order by their estimated beta. The ranked ETFs are assigned to one of two portfolios: low beta and high beta.
Each portfolio contains a quarter of the total assets.
def update(self, algorithm, data):
if data.contains_key("SPY"):
self.market_price.append(float(algorithm.securities["SPY"].price))
for key, value in self.assets.items():
if data.contains_key(key):
value.PRICE.append(float(algorithm.securities[key].price))
insights = []
if self.month != algorithm.time.month:
self.month = algorithm.time.month
beta_values = {}
market_return = np.diff(np.array(self.market_price))/np.array(self.market_price)[:-1]
long = None
for key, value in self.assets.items():
if key != "SPY" and len(value.PRICE) == value.PRICE.maxlen:
asset_return = np.diff(np.array(value.PRICE))/np.array(value.PRICE)[:-1]
beta_values[key] = self.beta(asset_return, market_return)
sorted_by_beta = sorted(beta_values, key = lambda x: beta_values[x])
The algorithm shorts the high-beta portfolio and longs the low-beta portfolio. Securities are rebalanced every calendar month.
The portfolio construction model is set to emit the target weight monthly.
The time period of insight is from the current day to the end of the calendar month.
In the Python calendar
library, calendar.monthrange(year, month)
returns the number of days in a month for the specified year and month.
The insight direction is set to be flat for symbols removed from the long/short list at the end of the month.
long = sorted_by_beta[:int(0.25*len(sorted_by_beta))]
short = sorted_by_beta[-int(0.25*len(sorted_by_beta)):]
# day: the weekday of first day of the month
# num_days: number of days in month
day, num_days = calendar.monthrange(algorithm.time.year, algorithm.time.month)
insight_period = num_days - algorithm.time.day - 1
if long and short:
invested = [x.key for x in algorithm.portfolio if x.value.INVESTED]
for i in invested:
if algorithm.portfolio[i].is_long and i not in long:
insights.append(Insight.price(i, timedelta(days=1), InsightDirection.FLAT))
if algorithm.portfolio[i].is_short and i not in short:
insights.append(Insight.price(i, timedelta(days=1), InsightDirection.FLAT))
for i in long:
insights.append(Insight.price(i, timedelta(days=insight_period), InsightDirection.UP))
for i in short:
insights.append(Insight.price(i, timedelta(days=insight_period), InsightDirection.DOWN))
Derek Melchin
See the attached backtest for an updated version of the algorithm in PEP8 style.
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!