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))


Reference

  1. Quantpedia - Beta Factor in Country Equity Indexes