Back to Community
Simple Mean Reversion Strategy

I'm new to both Quantopian and trading in general, but I attempted to implement a mean reversion strategy. I'm buying the low performing stocks and selling the high performing stocks each day weighting the performance of everything relative to the other stocks in the portfolio.

At this point I'm trying to interpret the results, but I'm not entirely sure how of the results are from the algorithm and how much are simply a consequence of the stocks I selected.

If anyone has any questions, comments, or suggestions please let me know!

I've had a lot of fun learning and working with the platform.

Clone Algorithm
1070
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
from numpy import linalg as LA
import numpy as np

# Put any initialization logic here.  The context object will be passed to
# the other methods in your algorithm.
def initialize(context):
    context.stocks = [sid(2518), sid(5692), sid(16841), sid(19831), sid(26807), 
                        sid(3735), sid(25317), sid(6984), sid(1900), sid(13905)]
    #context.stocks = []
    context.max_notional =  1000000
    context.previous_prices = None
    context.day = None


# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    if context.previous_prices == None:
        context.previous_prices = np.array([data[stock].price for stock in context.stocks])
        return
    
    if context.day == None:
        context.day = data[context.stocks[0]].datetime
        return
    
    if data[context.stocks[0]].datetime.day == context.day.day:
        return
    
    # calculate and shift change from previous days price
    current_prices = np.array([data[stock].price for stock in context.stocks])
    pct_change = current_prices / context.previous_prices - 1
    
    # normalize
    
    norm_pct = pct_change / LA.norm(pct_change)
    
    buy_multiplier = 1
    for i in range(len(norm_pct)):
        stock = context.stocks[i]
        sell_amount = round(-1 * context.portfolio.positions[stock].amount * norm_pct[i] * norm_pct[i])
        buy_amount = buy_multiplier * round((context.portfolio.cash / data[stock].price) * norm_pct[i] * norm_pct[i])
        
        notional = context.portfolio.positions[stock].amount * data[stock].price
                
        if norm_pct[i] > 0 and abs(sell_amount) > 0 and notional > -context.max_notional:
            if norm_pct[i]**2 > 0.05:
                order(stock, sell_amount)

        if norm_pct[i] < 0 and buy_amount > 0 and notional < context.max_notional:
            if abs(norm_pct[i]) > 0.05:
                order(stock, buy_amount)
            
    context.previous_prices = current_prices
    context.day = data[context.stocks[0]].datetime
    
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.
8 responses

nice!

HI Michael,

I am looking at the source code of your algorithm and I am unable to follow the logic of your final for loop. Can you add some explanation/ comments to the code snippet?


    for i in range(len(norm_pct)):  
        stock = context.stocks[i]  
        sell_amount = round(-1 * context.portfolio.positions[stock].amount * norm_pct[i] * norm_pct[i])  
        buy_amount = buy_multiplier * round((context.portfolio.cash / data[stock].price) * norm_pct[i] * norm_pct[i])  
        notional = context.portfolio.positions[stock].amount * data[stock].price  
        if norm_pct[i] > 0 and abs(sell_amount) > 0 and notional > -context.max_notional:  
            if norm_pct[i]**2 > 0.05:  
                order(stock, sell_amount)

        if norm_pct[i] < 0 and buy_amount > 0 and notional < context.max_notional:  
            if abs(norm_pct[i]) > 0.05:  
                order(stock, buy_amount)  

Sure, let me try to explain what I was trying to do.

    for i in range(len(norm_pct)):  
        stock = context.stocks[i]  
        sell_amount = round(-1 * context.portfolio.positions[stock].amount * norm_pct[i] * norm_pct[i])  
        buy_amount = buy_multiplier * round((context.portfolio.cash / data[stock].price) * norm_pct[i] * norm_pct[i])  

I step through each stock that I'm watching, and compute how many of that stock I'd buy or sell given my current position, cash, and its performance. The amount to buy is the maximum number of the stocks I could buy (cash / stock price) weighted by its performance relative to the other stocks in my portfolio. The amount we want to sell is the amount of that stock we own weighted in the same way.

I'm not 100% convinced of the validity of this logic (I feel like I should be weighting the stocks that had gains and losses independently of each other, but I haven't implemented it yet). My reasoning was that if we assume the stock is mean reverting, it makes sense to buy more of the ones that had performed poorly, and quickly sell off more of what had been performing well. The weightings were my way of expressing this idea.

        if norm_pct[i] > 0 and abs(sell_amount) > 0 and notional > -context.max_notional:  
            if norm_pct[i]**2 > 0.05:  
                order(stock, sell_amount)

        if norm_pct[i] < 0 and buy_amount > 0 and notional < context.max_notional:  
            if abs(norm_pct[i]) > 0.05:  
                order(stock, buy_amount)  

The next part implements the buying and selling. We compute how much we would buy or would sell for every stock, but only buy or sell if it's normalized percent change is negative or positive.

We also don't want to submit a buy or sell order unless we have hit some kind of threshold for how much it changed. In cases where the total change was very small we just hold onto the stock. It may be worth considering the stock performance from the point where we bought for the case where we're selling, but I haven't gotten around to testing that.

I hope that's helpful, let me know if you have any other questions.

Thanks so much for that response. This helps a lot

Glad I could help!

I went ahead and implemented the changes I was thinking about, and it does seem to improve the algorithm. Check it out!

Clone Algorithm
1070
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
from numpy import linalg as LA
import numpy as np

# Put any initialization logic here.  The context object will be passed to
# the other methods in your algorithm.
def initialize(context):
    context.stocks = [sid(2518), sid(5692), sid(16841), sid(19831), sid(5061), 
                        sid(3735), sid(25317), sid(6984), sid(1900), sid(13905)]
    #context.stocks = []
    context.max_notional =  1000000
    context.previous_prices = None
    context.day = None


# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    if context.previous_prices == None:
        context.previous_prices = np.array([data[stock].price for stock in context.stocks])
        return
    
    if context.day == None:
        context.day = data[context.stocks[0]].datetime
        return
    
    if data[context.stocks[0]].datetime.day == context.day.day:
        return
    
    # calculate and shift change from previous days price
    current_prices = np.array([data[stock].price for stock in context.stocks])
    
    
    pct_change = current_prices / context.previous_prices - 1
    
    gains = pct_change * (pct_change > 0)
    loss = pct_change * (pct_change <= 0)
    # normalize
    
    norm_gains = gains / LA.norm(gains)
    norm_loss = loss / LA.norm(loss)
    
    
    free_cash = context.portfolio.cash * 0.9
    
    for i in range(len(norm_gains)):
        stock = context.stocks[i]
        sell_amount = round(-1 * context.portfolio.positions[stock].amount * (norm_gains[i]**2))
                
        if norm_gains[i] > 0 and abs(sell_amount) > 0:
            if norm_gains[i]**2 > 0.33:
                order(stock, sell_amount)
            
    for i in range(len(norm_loss)):
        stock = context.stocks[i]
                
        buy_amount = round((free_cash / data[stock].price) * (norm_loss[i]**2))
        if norm_loss[i] < 0 and abs(buy_amount) > 0:
            if norm_loss[i]**2 > 0.33:
                order(stock, buy_amount)
        
    context.previous_prices = current_prices
    context.day = data[context.stocks[0]].datetime
    
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.

Nice improvement!

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.

Interesting.

I thought I'd try backtesting the algorithm against the top ten dividend yielding stocks in the NYSE for the last twelve months, and got this result: considerably worse!

Clone Algorithm
21
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
from numpy import linalg as LA
import numpy as np

# Put any initialization logic here.  The context object will be passed to
# the other methods in your algorithm.
def initialize(context):
    context.stocks = [sid(42548), sid(44688), sid(11461),sid(41817),
                     sid(42708), sid(42802), sid(41214), sid(43597),
                     sid(1601), sid(43315)]
    #context.stocks = []
    context.max_notional =  1000000
    context.previous_prices = None
    context.day = None


# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    if context.previous_prices == None:
        context.previous_prices = np.array([data[stock].price for stock in context.stocks])
        return
    
    if context.day == None:
        context.day = data[context.stocks[0]].datetime
        return
    
    if data[context.stocks[0]].datetime.day == context.day.day:
        return
    
    # calculate and shift change from previous days price
    current_prices = np.array([data[stock].price for stock in context.stocks])
    
    
    pct_change = current_prices / context.previous_prices - 1
    
    gains = pct_change * (pct_change > 0)
    loss = pct_change * (pct_change <= 0)
    # normalize
    
    norm_gains = gains / LA.norm(gains)
    norm_loss = loss / LA.norm(loss)
    
    
    free_cash = context.portfolio.cash * 0.9
    
    for i in range(len(norm_gains)):
        stock = context.stocks[i]
        sell_amount = round(-1 * context.portfolio.positions[stock].amount * (norm_gains[i]**2))
                
        if norm_gains[i] > 0 and abs(sell_amount) > 0:
            if norm_gains[i]**2 > 0.33:
                order(stock, sell_amount)
            
    for i in range(len(norm_loss)):
        stock = context.stocks[i]
                
        buy_amount = round((free_cash / data[stock].price) * (norm_loss[i]**2))
        if norm_loss[i] < 0 and abs(buy_amount) > 0:
            if norm_loss[i]**2 > 0.33:
                order(stock, buy_amount)
        
    context.previous_prices = current_prices
    context.day = data[context.stocks[0]].datetime
    
There was a runtime error.

good work!