Standard deviation based system (including standard deviation based TP and SL)

Thought I'd share another algorithm I was working on :P (having fun learning python/trying out ideas)

This one uses standard deviation to calculate it's stop loss and take profit, as well as to try and detect upward breaks from ranges (which trigger it's buy positions).

This is in line with winning percentages from most other volatile stocks I tried the algorithm on (such as google), although some less volatile stocks (like microsoft) I tried didn't work so well, - managing to somehow hit the take profit at small losses - leaving them at -2% max loss (tests other than the below were tested for 1 year).

There's also a few other possibly useful code snippets inside...

Enjoy! (and feel free to post your own patches in this thread)

513
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
def initialize(context):

# Set what stock to trade
context.stock = sid(24)

# Variable to hold opening price of long trades
context.long_open_price = 0

# Variable to hold stoploss price of long trades
context.long_stoploss = 0

# Variable to hold takeprofit price of long trades
context.long_takeprofit = 0

# Allow only 1 long position to be open at a time
context.long_open = False

# Initiate Tally of successes and fails

# Initialised at 0.0000000001 to avoid dividing by 0 in winning_percentage calculation
# (meaning that reporting will get more accurate as more trades are made, but may start
# off looking strange)

context.successes = 0.0000000001
context.fails = 0.0000000001

# Variable for emergency plug pulling (if you lose more than 30% starting capital,
# trading ability will be turned off... tut tut tut :shakes head dissapprovingly:)
context.plug_pulled = False

def handle_data(context, data):

# Reporting Variables
profit = 0
winning_percentage = context.successes / total_trades * 100

# Data Variables
starting_cash = context.portfolio.starting_cash
price = data[context.stock].price
vwap_5_day = data[context.stock].vwap(5)
equity = context.portfolio.cash + context.portfolio.positions_value
standard_deviation = data[context.stock].stddev(9)

# Set order size

# (Set here as "starting_cash/1000" - which coupled with the below
# "and price < 1000" - is a scalable way of setting (initially :P)
# affordable order quantities (for most stocks).

order_amount = starting_cash/1000

# Open Long Position if current price is larger than the 9 day volume weighted average
# plus 60% of the standard deviation (meaning the price has broken it's range to the
# up-side by 10% more than the range value)
if price > vwap_5_day + (standard_deviation * 0.6) and context.long_open == False and price < 1000:
order(context.stock,+order_amount)
context.long_open = True
context.long_open_price = price
context.long_stoploss = context.long_open_price - standard_deviation*0.6
context.long_takeprofit = context.long_open_price + standard_deviation*0.5
print 'Long Position Ordered'

# Close Long Position if takeprofit value hit

# Note that less volatile stocks can end up hitting takeprofit at a small loss
if price >= context.long_takeprofit and context.long_open == True:
order(context.stock,-order_amount)
context.long_open = False
context.long_takeprofit = 0
profit = (price*order_amount) - (context.long_open_price*order_amount)
context.successes = context.successes + 1
print 'Long Position Closed by Takeprofit at $%d profit' % (profit) print 'Total Equity now at$%d' % (equity)
print 'That leaves you with a winning percentage of %d percent' % (winning_percentage)

# Close Long Position if stoploss value hit
if price <= context.long_stoploss and context.long_open == True:
order(context.stock,-order_amount)
context.long_open = False
context.long_stoploss = 0
profit = (price*order_amount) - (context.long_open_price*order_amount)
context.fails = context.fails + 1
print 'Long Position Closed by Stoploss at $%d profit' % (profit) print 'Total Equity now at$%d' % (equity)
print 'That leaves you with a winning percentage of %d percent' % (winning_percentage)

# Pull Plug?

if equity < starting_cash*0.7:
context.plug_pulled = True
print "Ouch! We've pulled the plug..."

if context.plug_pulled == True and context.long_open == True:
order(context.stock,-order_amount)
context.long_open = False
context.long_stoploss = 0
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.
11 responses

Oops! I forgot to add " and context.plug_pulled == False" to the if statement that opens the trade (which prevents trading past a 30% loss of the starting cash value).

Add that code if you want to enable that feature :P

haha I also made a slight mistake in how I used the standard deviation in opening the position (been a year since I covered standard deviation in my 1st year stats module).

Below is the fix (which interestingly had a 1% higher winning percentage, but 5% lower profit).

513
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
def initialize(context):

# Set what stock to trade
context.stock = sid(24)

# Variable to hold opening price of long trades
context.long_open_price = 0

# Variable to hold stoploss price of long trades
context.long_stoploss = 0

# Variable to hold takeprofit price of long trades
context.long_takeprofit = 0

# Allow only 1 long position to be open at a time
context.long_open = False

# Initiate Tally of successes and fails

# Initialised at 0.0000000001 to avoid dividing by 0 in winning_percentage calculation
# (meaning that reporting will get more accurate as more trades are made, but may start
# off looking strange)

context.successes = 0.0000000001
context.fails = 0.0000000001

# Variable for emergency plug pulling (if you lose more than 30% starting capital,
# trading ability will be turned off... tut tut tut :shakes head dissapprovingly:)
context.plug_pulled = False

def handle_data(context, data):

# Reporting Variables
profit = 0
winning_percentage = context.successes / total_trades * 100

# Data Variables
starting_cash = context.portfolio.starting_cash
price = data[context.stock].price
vwap_5_day = data[context.stock].vwap(5)
equity = context.portfolio.cash + context.portfolio.positions_value
standard_deviation = data[context.stock].stddev(9)

# Set order size

# (Set here as "starting_cash/1000" - which coupled with the below
# "and price < 1000" - is a scalable way of setting (initially :P)
# affordable order quantities (for most stocks).

order_amount = starting_cash/1000

# Open Long Position if current price is larger than the 9 day volume weighted average
# plus 1.1 times the standard deviation (meaning the price has broken it's range to the
# up-side by 10% more than the range value)
if price > vwap_5_day + (standard_deviation*1.1) and context.long_open == False and price < 1000 and context.plug_pulled == False:
order(context.stock,+order_amount)
context.long_open = True
context.long_open_price = price
context.long_stoploss = context.long_open_price - standard_deviation*0.6
context.long_takeprofit = context.long_open_price + standard_deviation*0.5
print 'Long Position Ordered'

# Close Long Position if takeprofit value hit

# Note that less volatile stocks can end up hitting takeprofit at a small loss
if price >= context.long_takeprofit and context.long_open == True:
order(context.stock,-order_amount)
context.long_open = False
context.long_takeprofit = 0
profit = (price*order_amount) - (context.long_open_price*order_amount)
context.successes = context.successes + 1
print 'Long Position Closed by Takeprofit at $%d profit' % (profit) print 'Total Equity now at$%d' % (equity)
print 'That leaves you with a winning percentage of %d percent' % (winning_percentage)

# Close Long Position if stoploss value hit
if price <= context.long_stoploss and context.long_open == True:
order(context.stock,-order_amount)
context.long_open = False
context.long_stoploss = 0
profit = (price*order_amount) - (context.long_open_price*order_amount)
context.fails = context.fails + 1
print 'Long Position Closed by Stoploss at $%d profit' % (profit) print 'Total Equity now at$%d' % (equity)
print 'That leaves you with a winning percentage of %d percent' % (winning_percentage)

# Pull Plug?

if equity < starting_cash*0.7:
context.plug_pulled = True
print "Ouch! We've pulled the plug..."

if context.plug_pulled == True and context.long_open == True:
order(context.stock,-order_amount)
context.long_open = False
context.long_stoploss = 0
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.

Hi Adam ... My name is Abhi ... I am just starting out in the world of algorithmic trading ... Do you mind if I ask you a couple of basic questions about this algorithm?

Hi Abhi, feel free to ask questions and I'll do my best to answer.

Note that I'm quite new to algorithmic trading as well =)

Thanks... Most of these questions are very basic...I kinda feel like Alice in wonderland. :)

1. In lines 41-43 of your algorithm, why did you choose the vwrap window to be 5. Likewise why was the standard deviation window chosen to be 9?
2.  price > vwap_5_day + (standard_deviation * 0.6)  In this if block, why did you choose standard deviation & why the value of 0.6

Hi Abhi,

I'll try to explain the basic thinking behind the algorithm.

This algorithm was a basic experiment in "surfing" market sentiment. This means I was trying to detect breaks from normal market activity, and then "ride the wave" of changing price.

To try and detect normal market activity for a period, I used the vwap and standard deviation.

The vwap (volume weighted average price) represents the average price of the market over the given time period, weighted by the volume of trades people are making.

The standard deviation is a measure for a period of time of how spread out numbers are around the average (there's a good explanation here - http://www.mathsisfun.com/data/standard-deviation.html).

The basic assumption behind this algorithm is that when price remains within the range around the average given by the standard deviation, it is considered to be normal trading (and so not doing anything particularly interesting). Therefore, it assumes that when the price for the given asset "breaks" out of this normal trading range, that there is a change in normal market activity indicating either an upward or downward movement.

Upon a larger than normal (from the given date range) upward movement, this algorithm makes a buy order in order to ride the upward market sentiment. As prices tend to have a habit of changing by the value of the range they are breaking from, I conservatively set the algorithm to take it's profit upon an upward movement of half the standard deviation (0.5).

The problem with this system is that there can be false triggers, either when the market is still overall ranging but just about manages to break the historical range, or sometimes when a market is about to have a major move to the downside it will move up first (not sure why).

In order to reduce false triggers I set the if statement to only make a buy order after the range had been broken by 1 standard deviation + 0.1 (where it had broken the range, and then some extra). I accidentally in the first iteration above used half a standard deviation + 0.1 for this (0.6), but got away with it as I had set the standard deviation to more days than the average (the more days in the standard deviation, the larger it's likely potential size). The standard deviation had initially been set to more days than the average to make it slightly larger, and so make the algorithm less sensitive.

Knowing that this wasn't a 100% accurate way to detect continuing upward price movement, I also ensured to implement a stoploss to cut my losses when the algorithm was wrong, although being careful to not set it so close that it could be accidentally triggered.

Overall it seems the algorithm in more volatile (and numerically large) stocks seems to get it's assumptions of upward movement right more often than it get's them wrong (and so produces a profit in the backtested data).

Hopefully that explanation helps, and having just realised it's 4:30 am I think I'll call it a night :P

Thanks so much for the explanation .... Things are making sense now .... appreciate it

Adam, the mistake you are making is that you are backtesting on Apple. This is not a good stock to pick, because it's a gross case of hindsight bias. Apple's rise has been so strong that even a random buy/sell strategy would produce large profits.

A more appropriate test would be an index, or a stock that has not had the meteoric success Apple has over the last 5 years.

Ken, another user actually proved your point with a backtest that randomly buys Apple: https://www.quantopian.com/posts/random-long-apple-system-aka-the-dangers-of-foresight-bias

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.