Back to Community
Quantpedia Trading Strategy Series: An Analysis on Cross-Sectional Mean Reversion Strategies

The existence of short term return reversals for equities has captured the attention of many financial researchers.

In 1990, Bruce Lehmann found that over the period of 1962 - 1986 stocks in the highest returns of the prior week typically had negative returns in the following week. In his study, he found that contrarian strategies (picking past losers and winners) generated abnormal returns of over 2% each month. In the same year, Jegadeesh found that short-term reversals exist over the 1 month horizon. These 1 month short-term reversals are why many academic researchers generally use a 2-12 momentum measurement (returns over the past 12 months, excluding the previous one) when examining momentum.

Researchers have put forth a number of theories to try and explain short-term reversals. Lehmann attributed the phenomemon to cognitive bias leading to market inefficiency while another series of studies cited market-microstructure frictions (bid-ask bounce) as the cause.

Our Study

This notebook serves to analyze the findings on cross-sectional mean reversion strategies covered in various papers, during an out of sample period from 12-01-2011 to 12-01-2016. The study is done in two parts.

Part 1 specifically covers a review of the general contrarian strategies highlighted by Lehmann and Jegadeesh. Part 2 will cover more advanced and recently discovered contrarian strategies given to us by Quantpedia.

Our universe is defined as stocks in the Q1500 - I use the Q1500 as a proxy due to the liquidity and high market cap of most stocks in the universe.

Results Overview

In my notebook, I find that utilizing a decile grouping based on a returns lookback of 13 days is correlated with 13 day average returns, with the lowest/highest decile performing slightly over/under our SPY and Q1500 market benchmark with a 1.6% average spread per quarter. The actual implementation of short term reversal strategies suffers due to the high trading activity required to rebalance the portfolio. Due to this, the findings in many research papers fail to reflect the true profitability of short-term reversal strategies.

And while the performance is below the market, it's to be noted that this strategy is long/short compared to just long-only (which is the SPY). So if we want to compare total performance of that strategy, we should compare long only reversal of the "loser stocks decile". With that being said, this long/short strategy has a sharpe of .84 and relatively low vol.

Author's Notes

This two part series serves as a glance into the performance of cross-sectional mean reversion strategies in recent years. I encourage readers interested in these strategies to expand my analysis on the data generated in this notebook and experiment with optimizing portfolio construction for these strategies. Historically, most short term reversal strategies explored by researchers fail to reproduce the same performance found in studies during live trading, due to the substantial volume of stocks traded. More investigation in constructing a strategy to reduce portfolio turnover could substantially enhance the performance of various contrarian strategies.

FAQ

What is the Quantpedia Trading Strategy Series?

Quantpedia is an online resource for discovering trading strategies and we’ve teamed up with them to bring you interactive and high quality trading strategy examples based off financial research. Our goal is that you’re able to replicate the process we’ve used here for your own research and backtesting.

Where can I find more trading strategy ideas?

You can find the full Quantpedia Series here along with other research. Other than that, you can browse Quantpedia’s strategies or look through our forums for ideas posted by community members. Want to feature your own? Submit your proposal to SLEE @ quantopian.com

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.

2 responses

Simple Contrarian Trading Strategy

  1. For all stocks in the Q1500US, generate a factor representing the prior returns for a period of 13 trading days.
  2. Use the Portfolio Optimization API to construct a portfolio based off our factor, subject to our defined constraints.
  3. Rebalance on a weekly basis

Notes
This algorithm utilizes the Portfolio Optimization API with some simple constraints.
The code in the algorithm is commented so you can see each constraint that we place for portfolio construction.

Clone Algorithm
201
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
"""
Simple Cross-Sectional Mean Reversion Strategy
"""
import quantopian.algorithm as algo
import quantopian.experimental.optimize as opt

from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline, CustomFactor
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import AverageDollarVolume, Returns
from quantopian.pipeline.filters.morningstar import Q500US, Q1500US
from quantopian.pipeline.classifiers.morningstar import Sector
 
# Algorithm Parameters
# --------------------

# Constraint Parameters
MAX_GROSS_LEVERAGE = 1.0
MAX_SHORT_POSITION_SIZE = 0.015  # 1.5%
MAX_LONG_POSITION_SIZE = 0.015   # 1.5%

# Scheduling Parameters
MINUTES_AFTER_OPEN_TO_TRADE = 10
    
def initialize(context):
    # How many days we lookback to compute our returns deciles
    context.RETURNS_LOOKBACK = 13
    
    schedule_function(my_record_vars,
                      date_rules.every_day(),
                      time_rules.market_close())
    
    algo.attach_pipeline(make_pipeline(context), 'mean_reversion')

    # Schedule a function, 'do_portfolio_construction', to run once a week
    # ten minutes after market open.
    algo.schedule_function(
        do_portfolio_construction,
        date_rule=algo.date_rules.week_start(),
        time_rule=algo.time_rules.market_open(minutes=MINUTES_AFTER_OPEN_TO_TRADE),
        half_days=False,
    )
    
def make_pipeline(context):    
    mask = Q1500US()
    lb_13 = -Returns(window_length=13, mask=mask)
    
    pipe = Pipeline(
        columns={
            'alpha': lb_13,
            'sector': Sector(),
        },
        # combined_alpha will be NaN for all stocks not in our universe,
        # but we also want to make sure that we have a sector code for everything
        # we trade.
        screen=lb_13.notnull() & Sector().notnull(),
    )
    return pipe
 
def before_trading_start(context, data):
    """
    Called every day before market open.
    """
    context.pipeline_data = algo.pipeline_output('mean_reversion')
 
# Portfolio Construction
# ----------------------
def do_portfolio_construction(context, data):
    pipeline_data = context.pipeline_data
    todays_universe = pipeline_data.index

    # Objective
    # ---------
    # For our objective, we simply use our naive ranks as an alpha coefficient
    # and try to maximize that alpha.
    # 
    # This is a **very** naive model. Since our alphas are so widely spread out,
    # we should expect to always allocate the maximum amount of long/short
    # capital to assets with high/low ranks.
    #
    # A more sophisticated model would apply some re-scaling here to try to generate
    # more meaningful predictions of future returns.
    objective = opt.MaximizeAlpha(pipeline_data.alpha)

    # Constraints
    # -----------
    # Constrain our gross leverage to 1.0 or less. This means that the absolute
    # value of our long and short positions should not exceed the value of our
    # portfolio.
    constrain_gross_leverage = opt.MaxGrossLeverage(MAX_GROSS_LEVERAGE)
    
    # Constrain individual position size to no more than a fixed percentage 
    # of our portfolio. Because our alphas are so widely distributed, we 
    # should expect to end up hitting this max for every stock in our universe.
    constrain_pos_size = opt.PositionConcentration.with_equal_bounds(
        -MAX_SHORT_POSITION_SIZE,
        MAX_LONG_POSITION_SIZE,
    )

    # Constrain ourselves to allocate the same amount of capital to 
    # long and short positions.
    market_neutral = opt.DollarNeutral()
    
    # Constrain ourselve to have a net leverage of 0.0 in each sector.
    sector_neutral = opt.NetPartitionExposure.with_equal_bounds(
        labels=pipeline_data.sector,
        min=-0.0001,
        max=0.0001,
    )

    # Run the optimization. This will calculate new portfolio weights and
    # manage moving our portfolio toward the target.
    algo.order_optimal_portfolio(
        objective=objective,
        constraints=[
            constrain_gross_leverage,
            constrain_pos_size,
            market_neutral,
            sector_neutral,
        ],
        universe=todays_universe,
    )
    
def my_record_vars(context, data):
    record(leverage = context.account.leverage)
    record(num_pos = len(context.portfolio.positions))
    

There was a runtime error.

Nice work. One thing to try is to focus on saving transaction costs by a) limiting focus to top 100 most liquid/largest companies, b) including fees as a cost in the optimisation (for example maximise alpha after an estimate of fees).