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

def initialize(context):
    set_universe(universe.DollarVolumeUniverse(99, 100))
    context.dev_multiplier = 2
    context.max_notional = 1000000
    context.min_notional = -1000000
    context.days_traded = 0

    
def handle_data(context, data):
    context.days_traded += 1    

    dev_mult = context.dev_multiplier
    notional = context.portfolio.positions_value
    # Calls get_linear so that moving_average has something to reference by the time it is called
    linear = get_linear(data)
        
    # Only checks every 20 days
    if context.days_traded%20 == 0:
        try:
            for stock in data.keys():
                close = data[stock].price
                moving_average = linear[stock]
                moving_dev = data[stock].stddev(20)
                
                band = moving_average + dev_mult*moving_dev
                # If close price is greater than band, short 5000 and if less, buy 5000
                if close > band and notional > context.min_notional:
                    order(stock, -5000)
                    log.debug("Shorting 5000 of " + str(stock))
                elif close < band and notional < context.max_notional:
                    order(stock, 5000)
                    log.debug("Going long 5000 of " + str(stock))
        except:
            return
        
@batch_transform(window_length=20)
def get_prices(data):
    return data['price']

# Linear regression curve that returns the intercept the curve
# Uses the past 20 days
def get_linear(data):
    days = [i for i in range(1,21)]
    stocks = {}
    if get_prices(data) is None:
        return
    for stock in data:
        linear = stats.linregress(days, get_prices(data)[stock])[1]
        stocks[stock] = linear
    return stocks
    
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
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
'''
	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

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

    
def handle_data(context, data):
    context.days_traded += 1    

    dev_mult = context.dev_multiplier
    notional = context.portfolio.positions_value
    # Calls get_linear so that moving_average has something to reference by the time it is called
    linear = get_linear(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 of " + str(stock))
            elif close < low_band and notional < context.max_notional:
                order(context.stock, 5000)
                log.debug("Going long 5000 of " + str(stock))
        except:
            return
        
@batch_transform(window_length=20)
def get_prices(data):
    return data['price']

# Linear regression curve that returns the intercept the curve
# Uses the past 20 days
def get_linear(data):
    days = [i for i in range(1,21)]
    stocks = {}
    # Checks if data is emtpty
    if get_prices(data) is None:
        return
    for stock in data:
        linear = stats.linregress(days, get_prices(data)[stock])[1]
        stocks[stock] = linear
    return stocks
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
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
'''
	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

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

    
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
    # Calls get_linear so that moving_average has something to reference by the time it is called
    linear = get_linear(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
        
@batch_transform(window_length=20)
def get_prices(data):
    return data['price']

# Linear regression curve that returns the intercept the curve
# Uses the past 20 days
def get_linear(data):
    days = [i for i in range(1,21)]
    stocks = {}
    # Checks if data is emtpty
    if get_prices(data) is None:
        return
    for stock in data:
        linear = stats.linregress(days, get_prices(data)[stock])[1]
        stocks[stock] = linear
    return stocks
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
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
'''
	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

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
    context.exit_ind = "neutral"

    
def handle_data(context, data):
    
    context.days_traded += 1    
    dev_mult = context.dev_multiplier
    notional = context.portfolio.positions_value
    linear = get_linear(data)
    
    if get_prices(data) is None:
        return
    moving_average = linear[context.stock]
    current_position = abs(context.portfolio.positions_value)/data[context.stock].price
    
    # Checks to exit the position if the price crosses the average
    if context.exit_ind != "neutral":
        exit_position(context, data, current_position, moving_average)
    # Only checks every 20 days
    if context.days_traded%20 == 0:
        try:
            # Uses context.stock
            close = data[context.stock].price
            moving_dev = data[context.stock].stddev(20)
            
            high_band = moving_average + dev_mult*moving_dev
            low_band = moving_average - dev_mult*moving_dev
            
            log.debug("high band " + str(high_band))
            log.debug("low band " + str(low_band))
            
            # 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 at " + str(close))
                context.exit_ind = "short"
            elif close < low_band and notional < context.max_notional:
                order(context.stock, 5000)
                #log.debug("Going long 5000 at " + str(close))
                context.exit_ind = "long"
        except:
            return
        
def exit_position(context, data, amount, average):
    if context.exit_ind == "long":
        if data[context.stock].price >= average:
            order(context.stock, -amount)
            context.exit_ind = "neutral"
            #log.debug("exiting from long at " + str(average))
        else:
            pass
    elif context.exit_ind == "short":
        if data[context.stock].price <= average:
            order(context.stock, amount)
            context.exit_ind = "neutral"
            #log.debug("exiting from short at " + str(average))
        else:
            pass
        
@batch_transform(window_length=20)
def get_prices(data):
    return data['price']

# Linear regression curve that returns the intercept the curve
# Uses the past 20 days
def get_linear(data):
    days = [i for i in range(1,21)]
    stocks = {}
    # Checks if data is emtpty
    if get_prices(data) is None:
        return
    for stock in data:
        linear = stats.linregress(days, get_prices(data)[stock])[1]
        stocks[stock] = linear
    return stocks
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
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
'''
	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
    
    # Adds daily prices to an array of past prices
    context.past_prices = []

    
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
    # Calls get_linear so that moving_average has something to reference by the time it is called
    linear = get_linear(context, data)
    
    # Adds daily close prices to an array of past prices
    get_prices(context, data[context.stock].price)
        
    # 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

# Gets daily close prices for minutely backtesting        
def get_prices(context, price):
    if price == None:
        return
    # Will only add prices if it's the end of day
    if endofday_check(context, context.minutes_early):
        context.past_prices.append(price)
    # Adjusts the length on a rolling basis so that only the past twenty days are recorded
    if len(context.past_prices) > 20:
        del context.past_prices[0]

# 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)[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
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
620
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
    #make money on speculators i.e the fools
    #uses the price model that stock P will develop as
    # P[N+1] = average[W](P[N]) + K * average[W](P[N-1]) 
    #i.e simple taylor polynomial estimation of price
    #using a historical average and and average on the current price
    #
    #this algo is not using history as the implementation
    #requires specifying the window as a fix integer i.e
    # history(bar_count=horizon, frequency='1d', field='price')
 
#    
#Day based history window
#
class historicalData:
    
    def __init__(self,horizon):
        million = 10**6
        self.window = range(million,million + horizon)
        self.wtimedelta = range(0,horizon)
        self.cursor = 0
        self.avght = 0
        self.avgt = 0
        self.size = 0
        
            
    def getOldPrice(self,where=0):
        #where = where
        if(where > len(self.window) or (where < 0)):
            return 999999,0
        place = (self.cursor + where) % len(self.window)
        return self.window[place],self.wtimedelta[place]

    def getCurrentPrice(self,where=0):
        if(where > len(self.window) or (where < 0)):
            return 999999,0
        place = (self.cursor - where-1) % len(self.window)
        if(place < 0):
            place = place + len(self.window)
        return self.window[place],self.wtimedelta[place]       
    
    def insertData(self,price,timestamp):
        self.window[self.cursor]    = price
        self.wtimedelta[self.cursor] = timestamp
        self.cursor = (self.cursor + 1) % len(self.window)
        self.size = self.size +1
        if(self.size > len(self.window)):
            self.size = len(self.window)
        
    
    #
    #Get historical average 
    #
    def getHistAvg(self,delta):
        oprice,when =  self.getOldPrice()
        timeframe = when + delta
        maxc      = len(self.window) -1
        ntimer = 0
        aggregate = 0
        iters     = 0
        self.avght = 0
        while(maxc > 0 and ntimer < timeframe):
            val,ntimer = self.getOldPrice(iters)
            maxc = maxc -1
            iters = iters +1
            aggregate = aggregate + val
            self.avght = self.avght +1
        if(iters>0):
            return aggregate / iters
        else:
            return 0    


    #
    #Get current average
    #
    def getCurrentAvg(self,delta):
        oprice,when =  self.getCurrentPrice()
        timeframe = when - delta
        maxc      = len(self.window) -1
        ntimer = when
        aggregate = 0
        iters     = 0
        self.avgt = 0
        while(maxc > 0 and ntimer > timeframe):
            val,ntimer = self.getCurrentPrice(iters)
            maxc = maxc -1
            iters = iters +1
            aggregate = aggregate + val
            self.avgt = self.avgt + 1
        if(iters>0):
            return aggregate / iters
        else:
            return 0         
        
    def filled(self):
        return self.size == len(self.window)
    
    def avg(self):
        return self.avght,self.avgt
    
    def getLastInsert(self):
        price,when = self.getCurrentPrice()
        return when
    
    def getFirstInsert(self):
        price,when = self.getOldPrice()
        return when
        
    def getTotalTheta(self):
        return timedelta(self.getLastInsert(),
                         self.getFirstInsert())
    
    
    
def initialize(context):

    context.stock = sid(24) #
    context.max_notional = 1000000.1
    context.min_notional = -1000000.0
    context.verbose     = 1
    context.horizon     = 250/2 # business days year 255 events around 125
    context.gap         = 0.1
    context.riskprofile = 1.0
    context.lasttimeframe = 0
    context.daysecs = 24*60*60
    context.cursor   = 0
    context.accelerator = context.riskprofile * context.portfolio.cash / context.horizon
    context.mvavg = context.daysecs * 30  
    context.wino = historicalData(context.horizon)

    
    
#
# buy, sell or liquidize stocks
#
#
    
def sell(notional,context,price,timedelta): 
    if(notional > context.min_notional):
        order(context.stock, int( - (timedelta * context.accelerator 
                                / price)))
        #if context.verbose:
        #    log.info("Selling %s" % (context.stock))
    
def buy(notional,context,price,timedelta):
    if( notional < context.max_notional):
        order(context.stock,int(timedelta * context.accelerator 
                                / price))
        #if context.verbose:
        #    log.info("Buying %s" % (context.stock))

def liquidizer(context,timedelta):
    pos = context.portfolio.positions[context.stock].amount
    pos = pos * (1 / float(context.horizon)) * context.riskprofile * timedelta * 0.9
    pos = int(pos)
    if(pos> 3 or pos < 3):
        order(context.stock,-1 * pos)

        
#
#handle ticks and time delta
#       
def getTicks(context,price,atDate):
    timenow = datetime2seconds(atDate)
    context.timedelta = timedelta(timenow,context.lasttimeframe)
    context.lasttimeframe = timenow
    atimedelta = context.timedelta
    secsday = 24*60*60 
    lasti  = context.wino.getLastInsert()
    if((lasti+secsday) < timenow): 
        context.wino.insertData(price,timenow)
    return atimedelta

def datetime2seconds(dtime):
    secsday = 24*60*60
    secshour = 60*60
    secsminute = 60
    secsmonth = secsday *30
    secsyear = secsmonth * 12
    return (dtime.year - 1970)*secsyear + secsmonth * dtime.month + secsday * dtime.day + secshour * dtime.hour + secsminute*dtime.minute + dtime.second


def timedelta(dtimeA,dtimeB):
    secsday = 24*60*60
    return (dtimeA - dtimeB) / float(secsday)


def handle_data(context, data):
       
    price = data[context.stock].price     
    atDate = data[context.stock].datetime
    timedelta = getTicks(context,price,atDate)
    anchPrice = context.wino.getHistAvg(context.mvavg)
    avgPrice = context.wino.getCurrentAvg(context.mvavg)
    theta   = context.wino.getTotalTheta() / 356.0
    growth  = ((avgPrice - anchPrice) / anchPrice) * \
                (1 / theta) * 0.25   
    notional = context.portfolio.positions[context.stock].amount * price
    gap = context.gap
    lowbound  = avgPrice * (1.0-gap) * growth + avgPrice
    highbound = avgPrice * (1.0+gap) * growth + avgPrice
    predict   = avgPrice * growth + avgPrice
    
    if(context.wino.filled()):
        #record(lowbound=lowbound,highbound=highbound,price=price)
        
        record(price=price,predict=predict,
               historicalAvgPrice=anchPrice,
               averagePrice=avgPrice) 
        
        #record(theta=theta)
        
        #ht,t = context.wino.avg()
        #record(ht=ht,t=t)
        #if price < (lowbound):
        if price < lowbound:
            buy(notional,context,price,timedelta)
        
        if price > highbound:
            sell(notional,context,price,timedelta)
         
        if(lowbound*1.1 < price < highbound*0.9):
            liquidizer(context,timedelta)
    else:
        pass
        #record(price=price)
                
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