Back to Community
Trading Strategy: Mean-reversion

Hello everyone,

This is a simple trading strategy that provides some core mean-reverting properties. It involves the following:

  • If the current price is greater than the upper bollinger band, sell the stock
  • If the current price is less than the lower bollinger band, buy the stock

The bollinger bands are calculated using an average +- multiplier*standard deviation. The average in this case, is calculated by a linear regression curve because a simple moving average is often a lagging indicator and becomes a big problem with long look-back periods.

Playing around with the look-back period can provide some interesting results, try it out!
Thoughts and suggestions are always welcome.

-Seong

More on the strategy can be found here

*Edit:

Updated code to fix high and low bollinger bands

high_band = moving_average + dev_mult*moving_dev  
                low_band = moving_average - dev_mult*moving_dev  

                # If close price is greater than band, short 5000 and if less, buy 5000  
                if close > high_band and notional > context.min_notional:  
                    order(stock, -5000)  
                    log.debug("Shorting 5000 of " + str(stock))  
                elif close < low_band and notional < context.max_notional:  
                    order(stock, 5000)  
                    log.debug("Going long 5000 of " + str(stock))  
Clone Algorithm
1774
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
# Backtest ID: 5249fc13a9263506df171f26
We have migrated this algorithm to work with a new version of the Quantopian API. The code is different than the original version, but the investment rationale of the algorithm has not changed. We've put everything you need to know here on one page.
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.
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.

22 responses

Can you rewrite this so we can backtest it against individual stocks rather than the whole market that would be appreciated!

Isaac,

Done! I also fixed where the lower bollinger band was missing. I've set it up just using the S&P500 but you can modify the sid to you're liking.

Clone Algorithm
421
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
# Backtest ID: 524a4167c4599606eb50d66e
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.

Seong, this is an fascinating algo.

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.

It looks like in your handle_data function you have "context.days_traded += 1". Doesn't this function run every minuet in a full backtest? Wouldn't that cause the check to happen every 20min as opposed to 20 days?

PR,

Thanks for mentioning that, I hadn't thought about how it would work in minutely data as I only tested it in daily data, but here's a way to test it in minutely data as well.

Clone Algorithm
217
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
# Backtest ID: 524c7e96a43940070c7b5e58
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.

I'm unaccustomed to reading Python code, so I may be missing something, but where is the "exit position" command in your code? I see you buying 5000 shares when you're below the lower threshold and selling when you're above the upper, but I don't see you exiting anywhere in the middle. I ask because, in the header, you say that positions are exited when the price crosses the moving average.

Also, are you using leverage here?

Another question: you say that you're using the intercept of the linear regression curve, but isn't the second returned value of linregress (indexed by the number 1) correspond to the slope of the regression line? Again, new to Python, so I could be very wrong.

Unlike the futures market, the long side of stock markets work quite differently than the short side, at least that is what I have seen. It is probably because we humans react differently to greed and to fear. The short sides are quick steep drops lasting for short periods, while the long side is more gradual climbs and lasts longer. Based on that, the mean reversions need different parameters to work on both short and long sides. I love to see the exchange of ideas and generosity of the able coders here.

Thank you everyone for sharing.

bcf,

To my knowledge, linregress 1 returns the intercept of the linregress line while [0] would return the slope , more here
And you're right about the exit position, there is none for now, will get on that soon. And yes, there is a bit of leverage used here although as to how much would depend on the order amount.

-Seong

Ah yes, you're right about linregress. From a statistical point of view, that is a very strange choice on their part.

Are you able to run the strategy without any leverage, so we could get an idea of what the returns would be in that situation. I ask because I've played with similar strategies that gave nowhere near the same performance as yours, but they've been unleveraged, so I want to make sure I'm making a fair comparison.

Thanks!
Frank

bcf,

Still working on the leverage, but I've incorporated exit positions into the algorithm and the returns are very different. If you'd like to find out more about leverage there's a Quantopian thread here as well. The current exit position is whenever the price crosses the mean, and I think there'd be a better exit position than that especially with the 20 day lookback period on that. If you have any suggestions on that, please feel free to post

Thanks,
Seong

Clone Algorithm
421
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
# Backtest ID: 5252d1b338c5de07451ece3a
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.

Bcf,

The latest backtest I've uploaded doesn't use leverage so you could use that as a good way to compare your tests to mine

Here's a way to adapt it to minutely data (which works!), by using a check to add prices only once per day (at the close) you can effectively store close prices into an array in order to perform the linear regression method

Clone Algorithm
217
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
# Backtest ID: 528c3af34a372a0c3f954a46
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.

this works better

Clone Algorithm
603
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
# Backtest ID: 535fca6e89844c070e51cd30
There was a runtime error.

Marco, sorry newbie here...but the algorithm you posted is very different than Seong Lee's linear regression method/code. It seems closer to https://www.quantopian.com/posts/simple-algo-that-tries-to-earn-money-on-speculators. Did you post in the wrong thread? Can you outline any new changes you made...it makes it easier to see the new code changes.

Hi Seong,

When I cloned and run your algorithm, I got the following warning... so, is there any reason why you used batch_transform here other than history ? Would you elaborate more on how batch_transform work here ?

" Warning batch_transform is deprecated, please use history instead."

Hi Nyan,

I created this algorithm before 'history()' was released. 'batch_transform' is very outdated and we don't recommend you to use it anymore, instead please use 'history()' which allows you to query for X amount of historical data starting from the backtester's current trading date.

So if you wanted the past 20 days of trading data you would do:

'prices = history(20, '1d', 'price')'

The last version that I have here uses history to query for past data, feel free to use this one instead.

'''
    Linear Regression Curves vs. Bollinger Bands  
    If Close price is greater than average+n*deviation, go short  
    If Close price is less than average+n*deviation, go long  
    Both should close when you cross the average/mean  
'''


import numpy as np  
from scipy import stats  
from pytz import timezone, utc  
from datetime import datetime, timedelta  
from zipline.utils.tradingcalendar import get_early_closes

def initialize(context):  
    # Enter sid here to use the algo with a single stock  
    context.stock = sid(8554)

    context.dev_multiplier = 2  
    context.max_notional = 1000000  
    context.min_notional = -1000000

    # Days traded to ensure that we trade only once every twenty days  
    context.days_traded = 0  
    # Using an unimportant date just to initialize current_day  
    context.current_day = 0  
    # Gets all early close dates  
    start = datetime(1993, 1, 1, tzinfo=utc)  
    end = datetime(2050, 1, 1, tzinfo=utc)  
    context.early_closes = get_early_closes(start,end).date  
    # Minutes before close that you want to execute your order, so this will execute at 3:55 PM only  
    context.minutes_early = 5  

    context.past_prices = None

def handle_data(context, data):  
    if context.current_day != get_datetime().day:  
        context.current_day = get_datetime().day  
        context.days_traded += 1    

    dev_mult = context.dev_multiplier  
    notional = context.portfolio.positions_value  
    context.past_prices = history(20, '1d', 'price')  
    # Calls get_linear so that moving_average has something to reference by the time it is called  
    linear = get_linear(context, data)  
    # Only checks every 20 days  
    if context.days_traded%20 == 0:  
        try:  
            # Uses context.stock  
            close = data[context.stock].price  
            moving_average = linear[context.stock]  
            moving_dev = data[context.stock].stddev(20)

            high_band = moving_average + dev_mult*moving_dev

            low_band = moving_average - dev_mult*moving_dev  
            # If close price is greater than band, short 5000 and if less, buy 5000  
            if close > high_band and notional > context.min_notional:  
                order(context.stock, -5000)  
                log.debug("Shorting 5000")  
            elif close < low_band and notional < context.max_notional:  
                order(context.stock, 5000)  
                log.debug("Going long 5000")  
        except:  
            return

# Linear regression curve that returns the intercept the curve  
# Uses the past 20 days  
def get_linear(context, data):  
    days = [i for i in range(1,21)]  
    stocks = {}  
    # Checks if data is emtpty  
    if len(context.past_prices) < 20:  
        return  
    for stock in data:  
        linear = stats.linregress(days, context.past_prices[stock])[1]  
        stocks[stock] = linear  
    return stocks

# Returns True if it's the end of day and False otherwise  
def endofday_check(context, minutes_early):  
    # Converts all time-zones into US EST to avoid confusion  
    loc_dt = get_datetime().astimezone(timezone('US/Eastern'))  
    date = get_datetime().date()  
    # Checks for an early close on special dates such as holidays and the day after thanksgiving  
    # The market closes at 1:00PM EST on those days  
    if date in context.early_closes:  
        # Returns true if it's 1:00PM - minutes so in this case 12:55PM  
        if loc_dt.hour == 12 and loc_dt.minute == (44-minutes_early):  
            return True  
        else:  
            return False  
    # Returns true if it's 4:00PM EST - minutes so in this case at 3:40PM  
    # Daylight savings time are accounted for, so it will automatically adjust to DST  
    elif loc_dt.hour == 15 and loc_dt.minute == (44-minutes_early):  
        return True  
    else:  
        return False  

Seong

Hi Seong,
Well I feel that if rather than buying when close price crosses lower Bollinger for the first time, you should buy it once close price resurfaces and equals the lower Bollinger (and similarly for shorting also).

Have you ever heard of overfitting? The algorithm doesn't perform well on untrained/unseen data. Try e.g. to run the algorithm from 2013-2016. Walk-forward testing among other things are needed! ;-)

Hi Slgja,

The strategy was published on October 2nd, 2013!

Seong

Can anyone help me change this algo to something smaller? Every time I try to adjust it to say 3,000 it gives me a return of 29,000%. What's up with that? BTW I'm a total noob

Hey Frank,

The problem here is probably related to your order, being way too large. What's happening is that you are buying and selling lots of 3000 shares which makes your strategy unreasonable. For a good ressource on order types, try:

https://www.quantopian.com/help#ide-ordering

Nicolas