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

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

def handle_data(context, data):

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

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

def handle_data(context, data):

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

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

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

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

def handle_data(context, data):

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

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

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

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

636
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))

if( notional < context.max_notional):
order(context.stock,int(timedelta * context.accelerator
/ price))
#if context.verbose:

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:

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 ?

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

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

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