top of page

Correlation Index at the Top (II)

We have seen that there are times when those companies at the top of the SPY index in terms of capitalization move in lockstep and times when they diverge in their movements. Out investigation on this index is in our previous post. We were taking notice of this original article in Bloomberg.


This post will simulate some position-taking according to the signal generated by this index through a backtest using Quantconnect backtest platform. We will just set a fixed lower correlation value to decide that the market has had enough uncertainty and that we are leaving our positions for cash; initially, we will not pair this correlation at the top index with anything else to see how it reacts by itself. We will investigate the model's sensitivity to its parameters using Quantconnect optimization tool in our follow-up post. Later we will pair this correlation at the top, if it proves useful, with momentum and volatility so that we have all of the quantitative "figures of merit" of the market.


We use our flexible universe selection model; we will take the top N companies by traded volume to approximate total market capitalization for simplicity. We first initialize our algorithm:

class CorrAtTop(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2016, 5, 1)
        self.SetEndDate(datetime.today())
        self.SetCash(1000000)
        self.SetBrokerageModel(AlphaStreamsBrokerageModel())
        res = Resolution.Daily
        self.market = self.AddEquity('SPY', res).Symbol
        # Universe selection parameters:
        try:
            self.n_stocks = int(self.GetParameter("n_stocks"))
        except:
            self.n_stocks = 15
            self.UniverseSettings.Resolution = res
        universe = fusm(n_fine=self.n_stocks)
        self.AddUniverseSelection(universe)
        # Risk control parameters:
        try:
            self.rc = float(self.GetParameter("risk_factor"))
        except:
            self.rc = 0.03       
                        
        self.SetRiskManagement(
                 TrailingStopRiskManagementModel(self.rc))

        self.AddAlpha(CorrAtTopAlphaModel(self.market,
                                          self.rc))
                                          
        self.SetPortfolioConstruction(
        InsightWeightingPortfolioConstructionModel())

        self.SetExecution(ImmediateExecutionModel())

There is nothing complex in our algorithm initialization; set the start date to approximately 5 years ago, today as the test limit. We set the initial equity, the brokerage model, and the resolution of data to daily. We will try to grab the number of stocks in the universe from the parameter list of the algorithm; if there are none, we revert to the defaults of 15 stocks and a risk factor of 3% that we are not going to use today. We leave these parameter grabs here even if they do nothing today; next week, we will use them to build a parameter grid.


For our alpha model, our signal generation module, we have these values:

class CorrAtTopAlphaModel(AlphaModel):
    """    
    """
    def __init__(self, market, risk):
        self.symbol_data = {}
        self.market = market
        # Approximate 3-month correlation:
        self.period = 60
        self.Name = 'Correlation at Top'
        self.fut_ret = risk  # Future returns are not calculated.
        self.counter = False
        self.refresh = 2

We take the symbol that will act as our market, SPY, in this case, the period to calculate the realized correlation at (60 days, simulating 3 trading months) and the counters control the refreshing of the correlation parameter: every 2 days, in this case, to speed up the test.


Our Update method looks like this:

def Update(self, algorithm, data):

        insights = []
        if not data:
            return []
        symbols = data.keys()

        if not self.counter or self.counter%self.refresh==0:
            if self.market in symbols: symbols.remove(self.market)
            price = algorithm.History(symbols, self.period,
             Resolution.Daily).unstack(level=0)['close']
            self.corr = price.corr().mean().mean()
            algorithm.Debug(str(len(symbols)))
            algorithm.Plot("corr", "Correlation", self.corr)
        
        # Inelegant counter, to be replaced by
        # timer.
        self.counter += 1
        if self.corr < 0.2:
            direction = InsightDirection.Flat
            algorithm.Debug('Low Correlation, dropping positions.')
        else:
            direction = InsightDirection.Up
    
        p = timedelta(days=self.refresh)
        active = algorithm.ActiveSecurities.Values

        insights.append(Insight(self.market, p, InsightType.Price,
                                    direction, self.fut_ret, 1,
                                     self.Name, 1))

        return insights

If there is no data, for any reason, we do not act for the day. If there is data, we grab the ticker symbols we have received and call for the history of the closing prices. Then we extract the mean as we did in our previous post. We increase a very inelegant counter to skip the calculation every other day; if the correlation is below 0.2, our insights will be "flat" to the market, we will exist our position, while it is greater than 0.2, we will hold a full position in the market. This is not optimal by any chance; it serves only to check the behavior of the correlation at the top. We will plot this correlation to check that it is within the expected values and to add an image to this post that would otherwise be a wall of text mostly ignored by our new masters, the search engines.


Note that we try to remove the market symbol itself from the ticker lists (if self.market in symbols: symbols.remove(self.market)) as it will distort a little the correlation matrix with redundant values.


With these parameters, the equity curve for a 5-year test is:


The curve is not bad from a returns point of view. It is also not good from a risk-reward point of view. The correlation at the top indicator failed when it was needed the most, the first trimester of 2020. The model barely beats passively holding SPY for the same period. It really does a good job of keeping itself out of trouble at the beginning of 2018, and that is it. With a Sharpe ratio of 0.88 and a drawdown of 25%, we cannot be happy with its performance.


This is the correlation profile at the top, generating these results:


And it looks good. And looks good while ignoring everything else going on with the market. The addition of some information on momentum and volatility may improve the indicator's performance; we will check this in the future. The model, as it currently is, is at the very end of the post.


Information in ostirion.net does not constitute financial advice; we do not hold positions in any of the companies or assets that we mention in our posts at the time of posting. If you require quantitative model development, deployment, verification, or validation, do not hesitate and contact us. We will also be glad to help you with your machine learning or artificial intelligence challenges when applied to asset management, trading, or risk evaluations.



79 views0 comments
bottom of page