Back to Community
Enhancing Mean Reversion Algorithms

Mean-reversion is one of the most widely known trading strategies in quantitative finance. In his post about Enhancing Short-Term Mean-Reversion Strategies, Rob Reider discusses his experience working with strategies rooted in mean-reversion, and suggests ways in which a standard mean-reversion strategy can be augmented.

Extending from that, I explored enhancing a mean-reversion strategy using Alpha Vertex’s PreCog 500 data set. After studying the original mean-reversion algorithm found here, I developed the following hypothesis. Alpha Vertex’s 5 day forecasts could be used to discern which stocks to go long and short on in a mean-reversion strategy, and lead to less instances of a position being closed after the stock reverting.

The methodology I followed is as such. First, after researching the Alpha Vertex data and finding enough overlap between the group of stocks covered here and the Q500 universe, I divided the forecast returns into quantiles. Then, in addition to using standard mean-reversion logic to determine which stocks I should take long and short positions in, I added stocks in the highest and lowest quantile of forecast returns to the long and short baskets respectively.

Implementing this strategy resulted in slightly higher returns over the same time period than the original algorithm. Also, other metrics, including Beta, Sharpe Ratio, and Drawdown had more satisfactory values. Also, it may be worth noting that volatility was also lower.

I’m curious about exploring these results further and would love to hear from the community. I’d love to hear ways my implementation of this algorithm could be improved. Thanks in advance.

Loading notebook preview...
Notebook previews are currently unavailable.
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.

4 responses

Here are the results of the backtest.

Clone Algorithm
328
Loading...
Backtest from to with initial capital
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
"""

This algorithm enhances a simple five-day mean reversion strategy by:
1. Skipping the last day's return
2. Sorting stocks based on the volatility of the five day return, to get steady moves vs jumpy ones
I also commented out two other filters that I looked at: 
1. Six month volatility
2. Liquidity (volume/(shares outstanding))

"""

import numpy as np
import pandas as pd
from quantopian.pipeline import Pipeline
from quantopian.pipeline import CustomFactor
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import SimpleMovingAverage, AverageDollarVolume
from quantopian.pipeline.data import morningstar
from quantopian.pipeline.filters import Q500US
import quantopian.experimental.optimize as opt
import quantopian.algorithm as algo

from quantopian.pipeline.data.alpha_vertex import precog_top_500 as precog

MAX_GROSS_LEVERAGE = 1.0

def initialize(context):
    
    # Set benchmark to short-term Treasury note ETF (SHY) since strategy is dollar neutral
    set_benchmark(sid(23911))
    
    # Schedule our rebalance function to run at the end of each day.
    schedule_function(my_rebalance, date_rules.every_day(), time_rules.market_close())

    # Record variables at the end of each day.
    schedule_function(my_record_vars, date_rules.every_day(), time_rules.market_close())
    
    # Get intraday prices today before the close if you are not skipping the most recent data
    schedule_function(get_prices,date_rules.every_day(), time_rules.market_close(minutes=5))
    
    # Set commissions and slippage to 0 to determine pure alpha
    set_commission(commission.PerShare(cost=0, min_trade_cost=0))
    set_slippage(slippage.FixedSlippage(spread=0))
    
    # Number of quantiles for sorting returns for mean reversion
    context.nq = 5
    
    # Number of quantiles for sorting volatility over five-day mean reversion period
    context.nq_vol = 3
    
    # Create our pipeline and attach it to our algorithm.
    my_pipe = make_pipeline()
    attach_pipeline(my_pipe, 'my_pipeline')



class Volatility(CustomFactor):  
    inputs = [USEquityPricing.close]
    window_length = 132
    
    def compute(self, today, assets, out, close):
        # I compute 6-month volatility, starting before the five-day mean reversion period
        daily_returns = np.log(close[1:-6]) - np.log(close[0:-7])
        out[:] = daily_returns.std(axis = 0)           

class Liquidity(CustomFactor):   
    inputs = [USEquityPricing.volume, morningstar.valuation.shares_outstanding] 
    window_length = 1
    
    def compute(self, today, assets, out, volume, shares):       
        out[:] = volume[-1]/shares[-1]        

class Sector(CustomFactor):
    inputs = [morningstar.asset_classification.morningstar_sector_code]
    window_length = 1
    
    def compute(self, today, assets, out, sector):
        out[:] = sector[-1]  

class PredictionQuality(CustomFactor):
    """
    create a customized factor to calculate the prediction quality
    for each stock in the universe.
    
    compares the percentage of predictions with the correct sign 
    over a rolling window (3 weeks) for each stock
   
    """
    # data used to create custom factor
    inputs = [precog.predicted_five_day_log_return, USEquityPricing.close]

    # change this to what you want
    window_length = 15

    def compute(self, today, assets, out, pred_ret, px_close):
        # actual returns
        px_close_df = pd.DataFrame(data=px_close)
        pred_ret_df = pd.DataFrame(data=pred_ret)
        log_ret5_df = np.log(px_close_df) - np.log(px_close_df.shift(5))

        log_ret5_df = log_ret5_df.iloc[5:].reset_index(drop=True)
        n = len(log_ret5_df)
        
        # predicted returns
        pred_ret_df = pred_ret_df.iloc[:n]

        # number of predictions with incorrect sign
        err_df = (np.sign(log_ret5_df) - np.sign(pred_ret_df)).abs() / 2.0

        # custom quality measure
        pred_quality = (1 - pd.ewma(err_df, min_periods=n, com=n)).iloc[-1].values
        
        out[:] = pred_quality

class NormalizedReturn(CustomFactor):
    """
    Custom Factor to calculate the normalized forward return 
       
    Scales the forward return expecation by the historical volatility
    of returns
    
    This can be used to supplement the calculation of quantiles for sorting used in mean reversion (context.nq)
    """
    # data used to create custom factor
    inputs = [precog.predicted_five_day_log_return, USEquityPricing.close]
    
    # change this to what you want
    window_length = 10

    def compute(self, today, assets, out, pred_ret, px_close):
        # mean return 
        avg_ret = np.nanmean(pred_ret[-1], axis =0)
        
        # standard deviation of returns
        std_ret = np.nanstd(pred_ret[-1], axis=0)

        # normalized returns
        norm_ret = (pred_ret[-1] - avg_ret) / std_ret

        out[:] = norm_ret



def make_pipeline():
    """
    Create our pipeline.
    """
    covered_stocks = Q500US() & precog.predicted_five_day_log_return.latest.notnull()

    prediction_quality = PredictionQuality(mask=covered_stocks)
    quality = prediction_quality > 0.65
    normalized_return = NormalizedReturn(mask=quality)

    # create a pipeline of only stocks that are covered above
    pipe = Pipeline(
        columns={
            'pred 5 day returns' : precog.predicted_five_day_log_return.latest,
            'normalized returns' : normalized_return,
        },
        screen=covered_stocks
    )
    
    return pipe


def before_trading_start(context, data):
    # Gets our pipeline output every day.
    context.output = pipeline_output('my_pipeline')
       

def get_prices(context, data):
    # Get the last 6 days of prices for every stock in our universe
    Universe500 = context.output.index.tolist()
    prices = data.history(Universe500, 'price', 6, '1d')
    daily_rets = np.log(prices / prices.shift(1))

    rets = (prices.iloc[-2] - prices.iloc[0]) / prices.iloc[0]
    # I used data.history instead of Pipeline to get historical prices so you can have the 
    # option of using the intraday price just before the close to get the most recent return.
    # In my post, I argue that you generally get better results when you skip that return.
    # If you don't want to skip the most recent return, however, use .iloc[-1] instead of .iloc[-2]:
    # rets = (prices.iloc[-1] - prices.iloc[0]) / prices.iloc[0]
    
    stdevs = daily_rets.std(axis=0)

    rets_df = pd.DataFrame(rets, columns=['five_day_ret'])
    stdevs_df = pd.DataFrame(stdevs, columns=['stdev_ret'])
    
    context.output = context.output.join(rets_df, how='outer')
    context.output = context.output.join(stdevs_df, how='outer')
    
    context.output['ret_quantile'] = pd.qcut(context.output['five_day_ret'], context.nq, labels=False) + 1
    context.output['stdev_quantile'] = pd.qcut(context.output['stdev_ret'], 3, labels=False) + 1
    
    context.output['norm_ret_quantile'] = pd.qcut(context.output['normalized returns'], context.nq, labels=False) + 1

    context.longs = context.output[((context.output['ret_quantile'] == 1) &
                                   (context.output['stdev_quantile'] < context.nq_vol)) |
                                   (context.output['norm_ret_quantile'] == 5)].index.tolist()
    context.shorts = context.output[((context.output['ret_quantile'] == context.nq) &
                                    (context.output['stdev_quantile'] < context.nq_vol)) |
                                    (context.output['norm_ret_quantile'] == 1)].index.tolist()    

    
def my_rebalance(context, data):
    """
    Rebalance daily.
    """
    weights = {}
    
    for security in context.longs:
        if data.can_trade(security):
            weights[security] = 0.5 / (len(context.longs))

    for security in context.shorts:
        if data.can_trade(security):
            weights[security] = -0.5 / (len(context.shorts))
    
    objective = opt.TargetPortfolioWeights(weights)
    
    leverage_constraint = opt.MaxGrossLeverage(MAX_GROSS_LEVERAGE)
    dollar_neutral_constraint = opt.DollarNeutral()
    
    algo.order_optimal_portfolio(
        objective=objective,
        constraints=[
            leverage_constraint,
            dollar_neutral_constraint
        ],
        universe=weights.keys()
    )


def my_record_vars(context, data):
    """
    Record variables at the end of each day.
    """
    longs = shorts = 0
    for position in context.portfolio.positions.itervalues():
        if position.amount > 0:
            longs += 1
        elif position.amount < 0:
            shorts += 1
    # Record our variables.
    record(leverage=context.account.leverage, long_count=longs, short_count=shorts)
    
    log.info("Today's shorts: "  +", ".join([short_.symbol for short_ in context.shorts]))
    log.info("Today's longs: "  +", ".join([long_.symbol for long_ in context.longs]))
There was a runtime error.

Hi Jeremy -

It is important to note that the Alpha Vertex PreCog dataset was loaded March 6, 2017 and thus most of the data are considered in-sample:

https://www.quantopian.com/posts/alpha-vertex-precog-dataset#58c31ee6125e066b29568aaa
https://www.quantopian.com/posts/quantopian-partner-data-how-is-it-collected-processed-and-surfaced

I see from the little "Q" next to your name that you are affiliated with Quantopian, so perhaps this is old news to you, but I thought I'd point it out for the masses. It doesn't necessarily invalidate your results, but in my opinion, they need to be taken with a grain of salt, until more out-of-sample data are available (the minimum used by the Q Fund team is 6 months).

Also, in your algo above, I'd point out that you are using:

    # Set commissions and slippage to 0 to determine pure alpha  
    set_commission(commission.PerShare(cost=0, min_trade_cost=0))  
    set_slippage(slippage.FixedSlippage(spread=0))  

Hi all,

I tried to clone this algo to learn more about the mean reversion strat utilised. I can see from the error log that the following import failed from line 23 (No module was found):

import precog_top_500 as precog

Any solutions? Thanks!

Hi Martin,

Unfortunately, AlphaVertex stopped updating the dataset in January 2018, so we removed the dataset from Quantopian. My suggestion would be to create your own factor and replace the dataset_500.predicted_five_day_log_return column with something you create. From a technical perspective, you should be able to put any pipeline factor in its place. Conceptually, I'm not sure what a suitably comparable factor would be, but there are several of other datasets that you can look through for ideas.

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.