Back to Community
Mean-Reversion Long - For Bold Contrarians

Simple mean reversion long only algorithm. Got the rules from a book by Laurens Bensdorp.

Objective
- Trade stocks that are in a long-term uptrend, that have significantly volatility and are oversold on the short term.
- Execute trades every day.
- Beat the overall market in both bull and sideways markets.

Trading universe
- All stocks from AMEX, NASDAQ, and NYSE.
- Do not trade ETFs, pink sheets or bulletin board stocks.

Filters
- Minimum Average Volume of the last 50 days is above 500.000 shares (ensure liquidity).
- Minimum price is 1 USD.
- Dollar volume is at least 2.500.000 USD (ensure worth trading).

Position Sizing:
- Maximum 10 positions.
- Fixed fractional risk: 2 percent.
- Maximum position size: 10 percent.

Buys
1. Trade every day.
2. Close of the stock is above the 150-daily SMA (long term uptrend).
3. Seven day average directional index (ADX) is above 45 (short term trend strength.
4. Average true range percent of the last 10 days is above 4 percent (volatility).
5. The three day RSI is below 30 (oversold on a short term basis).
6. Rank orders by the lowest three day RSI.

Sells:
1. Trade every day.
2. Sell when one of the following conditions is met:
2.1 Stop Loss: 2.5 times the ten-day ATR.
2.2 Profit Target: 3 percent or more.
2.3 Time Exit: 4 days have passed and none of the above conditions is met.

Notes:
- The algorithm tracks the available cash trying to avoid spending more than we have.
- The rebalance is split in 2 functions with an hour gap. This is just to "ensure" sell orders are filled before we start buying.
- Sometimes 2.5 times the ATR is higher than the stock price, in those cases we don’t place the stop loss.
- The book tries to buy more fear by placing the buy orders at 4 percent less than the previous day closing price.
- The only purpose of canceling open orders at the end of the trading day is to avoid the console warnings.
- The backtests shown in the book are better. That could be because the entry and exit rules are a bit different. However it does beat the S&P 500.

Other algorithms:
- Weekly Rotation S&P 500
- Mean-Reversion Short - Money in bad markets

Clone Algorithm
105
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
import quantopian.algorithm as algo
import numpy as np
import talib
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.filters import QTradableStocksUS, Q1500US
from quantopian.pipeline.factors import SimpleMovingAverage, RSI, CustomFactor, Returns, Latest, AverageDollarVolume 
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data import Fundamentals
from quantopian.pipeline.data import morningstar
from quantopian.pipeline.filters.morningstar import IsPrimaryShare
import copy
import math
import datetime as dt
from datetime import datetime, timedelta, date

class Signals:
    def __init__(self, d, a):
        self.days = d
        self.atr = a

class atr_10days_percent(CustomFactor):  
    inputs = [USEquityPricing.high, USEquityPricing.low, USEquityPricing.close]  
    window_length = 11  
    def compute(self, today, assets, out, high, low, close):  
        range_high = np.maximum(high[1:], close[:-1]) 
        range_low = np.minimum(low[1:], close[:-1])  
        out[:] = np.mean(((range_high - range_low)/close[:-1])*100, axis=0) 
        
class atr_10days(CustomFactor):
    inputs = [USEquityPricing.high, USEquityPricing.low, USEquityPricing.close]  
    window_length = 11  
    def compute(self, today, assets, out, high, low, close):  
        range_high = np.maximum(high[1:], close[:-1])
        range_low = np.minimum(low[1:], close[:-1])
        out[:] = np.mean(range_high - range_low, axis=0)  

def initialize(context):
    set_commission(commission.PerTrade(cost=0.00))
    set_slippage(slippage.FixedSlippage(spread=0.00))
    set_long_only()
 
    schedule_function(sell, date_rules.every_day(), time_rules.market_open(hours=0, minutes=2), half_days=False)
    schedule_function(buy, date_rules.every_day(), time_rules.market_open(hours=0, minutes=30), half_days=False)
    schedule_function(cancel_open_orders, date_rules.every_day(), time_rules.market_close(minutes=1), half_days=False)
    schedule_function(my_record_vars, date_rules.every_day(), time_rules.market_close(), half_days=False)
    algo.attach_pipeline(make_pipeline(), 'my_pipeline')
    
    context.tracker = dict()  
    context.tmp_tracker= dict()
    
def make_pipeline():
    exchange = Fundamentals.exchange_id.latest
    nyse_filter = exchange.eq('NYSE')
    amex_filter = exchange.eq('AMEX')
    nasdaq_filter = exchange.eq('NAS')
    filter_exchange = nasdaq_filter | amex_filter | nyse_filter
    
    have_market_cap = morningstar.valuation.market_cap.latest.notnull() 
    
    avg_volume = SimpleMovingAverage(inputs=[USEquityPricing.volume],window_length=50)
    filter_volume = avg_volume > 500000
    
    last_price = Latest(inputs=[USEquityPricing.close], window_length=1) 
    filter_price = last_price > 1
    
    dollar_volume = AverageDollarVolume(window_length=50)
    filter_dollar_volume = dollar_volume > 2500000
    
    sma_150 = SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=150)
    filter_sma_150 = USEquityPricing.close.latest > sma_150
    
    atr_10_percent = atr_10days_percent()
    filter_atr_10 = atr_10_percent > 4
    
    rsi = RSI(inputs=[USEquityPricing.close], window_length=3)
    filter_overbought = rsi < 30
    
    atr_10 = atr_10days()
    
    stocks_to_trade = filter_exchange & have_market_cap & filter_volume & filter_price & filter_dollar_volume & filter_sma_150 & filter_atr_10 & filter_overbought

    return Pipeline(
        columns = {
            'stocks': stocks_to_trade,
            'rsi': rsi,
            'atr': atr_10
        },
        screen = (stocks_to_trade)
    )

def before_trading_start(context, data):
    context.my_output = pipeline_output('my_pipeline')
    prepare_candidates(context, data)
    
def sell(context, data):
    clean_tracker(context, data)
    add_to_tracker(context, data)
    increment_day(context, data)
    for security in context.portfolio.positions:
        if data.can_trade(security): 
            age = context.tracker[security].days
            if is_expired(age):
                order_target_percent(security, 0)
            else:
                price_share = context.portfolio.positions[security].cost_basis
                atr = context.tracker[security].atr
                stop_loss_price = get_stop_price(price_share, atr)
                if stop_loss_price > 0:
                    order_target_percent(security, 0, style=StopOrder(stop_loss_price)) 
                
                profit_price = price_share + price_share * 0.03
                order_target_percent(security, 0, style=LimitOrder(limit_price=profit_price)) 
    context.tmp_tracker = dict()

def buy(context, data):
    cash_accum = 0;
    for security in context.candidates:
        if security not in context.portfolio.positions and data.can_trade(security): 
            if len(context.portfolio.positions) < 10:
                price_share = data.current(security,'close')
                cost = get_cost(security, context, data, price_share)
                if cost < (context.portfolio.cash - cash_accum) and is_trade(context, data):
                    order_value(security, cost, style=LimitOrder(limit_price=price_share))
                    context.tmp_tracker[security] = context.my_output.get_value(security, 'atr')
                    cash_accum = cash_accum + cost
                    
def cancel_open_orders(context, data):
    for s in get_open_orders():  
        for o in get_open_orders(s):  
            cancel_order(o)       
        
def my_record_vars(context, data):
    record(cash=context.portfolio.cash)

def compute_adx(stock, data):
    period = 7  
    h = data.history(stock,'high', 2*period,'1d').dropna()  
    l = data.history(stock,'low', 2*period,'1d').dropna()  
    c = data.history(stock,'close', 2*period,'1d').dropna() 
    ta_ADX = talib.ADX(h, l, c, period)  
    adx = ta_ADX[-1]
    return adx

def prepare_candidates(context, data): 
    my_out = context.my_output.copy(deep=True)
    to_remove = []
    for index, row in my_out.iterrows():        
        adx = compute_adx(index, data)
        if(adx < 45.0):
            to_remove.append(index)        
    my_out.drop(to_remove, inplace=True)   
    context.candidates = my_out.sort_values('rsi', ascending=True).head(10).index.tolist() 
    
def get_cost(security, context, data, price_share):
    atr = context.my_output.get_value(security, 'atr')       
    stop_loss = get_stop_price(price_share, atr)
    dollar_risk_share = price_share - stop_loss
    total_value = context.portfolio.portfolio_value
    cash_to_risk = total_value * 0.02
    num_shares = math.floor(cash_to_risk / dollar_risk_share)
    cost = num_shares * price_share
    max_cost = total_value * 0.1
    if cost > max_cost:
        num_shares = math.floor((total_value * 0.1) / price_share)     
    cost = num_shares * price_share
    return cost
    
def is_expired(days):
    return True if days > 3 else False

def clean_tracker(context, data):
    to_remove = copy.deepcopy(context.tracker)
    for sec in context.tracker:
        if not (sec in context.portfolio.positions):
           to_remove.pop(sec)          
    context.tracker = to_remove

def add_to_tracker(context, data):
    for security in context.portfolio.positions:
        if not (security in context.tracker):
            context.tracker[security] = Signals(0, context.tmp_tracker[security])
    
def increment_day(context, data):
    for sec in context.tracker:
        context.tracker[sec].days = context.tracker[sec].days + 1
    
def get_stop_price(price, atr):
    p = price - 2.5 * atr
    return 0 if p < 0 else p
                    
def is_trade(context, data):
    i = 0
    for s in get_open_orders():  
        for o in get_open_orders(s):  
            if o.amount > 0:
                i += 1
    i = len(context.portfolio.positions) + i 
    return False if i > 9 else True
There was a runtime error.
1 response

There was an error when selling at profit. I was using a limit order, this means it could get filled at less than the desired price. In the new version total return is slightly higher (+5%) but the drawdown is around 25% worse.

Clone Algorithm
105
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
import quantopian.algorithm as algo
import numpy as np
import talib
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.filters import QTradableStocksUS, Q1500US
from quantopian.pipeline.factors import SimpleMovingAverage, RSI, CustomFactor, Returns, Latest, AverageDollarVolume 
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data import Fundamentals
from quantopian.pipeline.data import morningstar
from quantopian.pipeline.filters.morningstar import IsPrimaryShare
import copy
import math
import datetime as dt
from datetime import datetime, timedelta, date

class Signals:
    def __init__(self, d, a):
        self.days = d
        self.atr = a

class atr_10days_percent(CustomFactor):  
    inputs = [USEquityPricing.high, USEquityPricing.low, USEquityPricing.close]  
    window_length = 11  
    def compute(self, today, assets, out, high, low, close):  
        range_high = np.maximum(high[1:], close[:-1]) 
        range_low = np.minimum(low[1:], close[:-1])  
        out[:] = np.mean(((range_high - range_low)/close[:-1])*100, axis=0) 
        
class atr_10days(CustomFactor):
    inputs = [USEquityPricing.high, USEquityPricing.low, USEquityPricing.close]  
    window_length = 11  
    def compute(self, today, assets, out, high, low, close):  
        range_high = np.maximum(high[1:], close[:-1])
        range_low = np.minimum(low[1:], close[:-1])
        out[:] = np.mean(range_high - range_low, axis=0)  

def initialize(context):
    set_commission(commission.PerTrade(cost=0.00))
    set_slippage(slippage.FixedSlippage(spread=0.00))
    set_long_only()
 
    schedule_function(sell, date_rules.every_day(), time_rules.market_open(hours=0, minutes=2), half_days=False)
    schedule_function(buy, date_rules.every_day(), time_rules.market_open(hours=0, minutes=30), half_days=False)
    schedule_function(cancel_open_orders, date_rules.every_day(), time_rules.market_close(minutes=1), half_days=False)
    schedule_function(my_record_vars, date_rules.every_day(), time_rules.market_close(), half_days=False)
    algo.attach_pipeline(make_pipeline(), 'my_pipeline')
    
    context.tracker = dict()  
    context.tmp_tracker= dict()
    
def make_pipeline():
    exchange = Fundamentals.exchange_id.latest
    nyse_filter = exchange.eq('NYSE')
    amex_filter = exchange.eq('AMEX')
    nasdaq_filter = exchange.eq('NAS')
    filter_exchange = nasdaq_filter | amex_filter | nyse_filter
    
    have_market_cap = morningstar.valuation.market_cap.latest.notnull() 
    
    avg_volume = SimpleMovingAverage(inputs=[USEquityPricing.volume],window_length=50)
    filter_volume = avg_volume > 500000
    
    last_price = Latest(inputs=[USEquityPricing.close], window_length=1) 
    filter_price = last_price > 1
    
    dollar_volume = AverageDollarVolume(window_length=50)
    filter_dollar_volume = dollar_volume > 2500000
    
    sma_150 = SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=150)
    filter_sma_150 = USEquityPricing.close.latest > sma_150
    
    atr_10_percent = atr_10days_percent()
    filter_atr_10 = atr_10_percent > 4
    
    rsi = RSI(inputs=[USEquityPricing.close], window_length=3)
    filter_overbought = rsi < 30
    
    atr_10 = atr_10days()
    
    stocks_to_trade = filter_exchange & have_market_cap & filter_volume & filter_price & filter_dollar_volume & filter_sma_150 & filter_atr_10 & filter_overbought

    return Pipeline(
        columns = {
            'stocks': stocks_to_trade,
            'rsi': rsi,
            'atr': atr_10
        },
        screen = (stocks_to_trade)
    )

def before_trading_start(context, data):
    context.my_output = pipeline_output('my_pipeline')
    prepare_candidates(context, data)
    
def sell(context, data):
    clean_tracker(context, data)
    add_to_tracker(context, data)
    increment_day(context, data)
    for security in context.portfolio.positions:
        if data.can_trade(security): 
            age = context.tracker[security].days
            if is_expired(age):
                order_target_percent(security, 0)
            else:
                price_share = context.portfolio.positions[security].cost_basis
                atr = context.tracker[security].atr
                stop_loss_price = get_stop_price(price_share, atr)
                if stop_loss_price > 0:
                    order_target_percent(security, 0, style=StopOrder(stop_loss_price)) 
                
                profit_price = price_share * 1.03
                last_price = context.portfolio.positions[security].last_sale_price
                if last_price >= profit_price:
                    order_target_percent(security, 0)
    context.tmp_tracker = dict()

def buy(context, data):
    cash_accum = 0;
    for security in context.candidates:
        if security not in context.portfolio.positions and data.can_trade(security): 
            if len(context.portfolio.positions) < 10:
                price_share = data.current(security,'close')
                cost = get_cost(security, context, data, price_share)
                if cost < (context.portfolio.cash - cash_accum) and is_trade(context, data):
                    order_value(security, cost, style=LimitOrder(limit_price=price_share))
                    context.tmp_tracker[security] = context.my_output.get_value(security, 'atr')
                    cash_accum = cash_accum + cost
                    
def cancel_open_orders(context, data):
    for s in get_open_orders():  
        for o in get_open_orders(s):  
            cancel_order(o)       
        
def my_record_vars(context, data):
    record(cash=context.portfolio.cash)

def compute_adx(stock, data):
    period = 7  
    h = data.history(stock,'high', 2*period,'1d').dropna()  
    l = data.history(stock,'low', 2*period,'1d').dropna()  
    c = data.history(stock,'close', 2*period,'1d').dropna() 
    ta_ADX = talib.ADX(h, l, c, period)  
    adx = ta_ADX[-1]
    return adx

def prepare_candidates(context, data): 
    my_out = context.my_output.copy(deep=True)
    to_remove = []
    for index, row in my_out.iterrows():        
        adx = compute_adx(index, data)
        if(adx < 45.0):
            to_remove.append(index)        
    my_out.drop(to_remove, inplace=True)   
    context.candidates = my_out.sort_values('rsi', ascending=True).head(10).index.tolist() 
    
def get_cost(security, context, data, price_share):
    atr = context.my_output.get_value(security, 'atr')       
    stop_loss = get_stop_price(price_share, atr)
    dollar_risk_share = price_share - stop_loss
    total_value = context.portfolio.portfolio_value
    cash_to_risk = total_value * 0.02
    num_shares = math.floor(cash_to_risk / dollar_risk_share)
    cost = num_shares * price_share
    max_cost = total_value * 0.1
    if cost > max_cost:
        num_shares = math.floor((total_value * 0.1) / price_share)     
    cost = num_shares * price_share
    return cost
    
def is_expired(days):
    return True if days > 4 else False

def clean_tracker(context, data):
    to_remove = copy.deepcopy(context.tracker)
    for sec in context.tracker:
        if not (sec in context.portfolio.positions):
           to_remove.pop(sec)          
    context.tracker = to_remove

def add_to_tracker(context, data):
    for security in context.portfolio.positions:
        if not (security in context.tracker):
            context.tracker[security] = Signals(0, context.tmp_tracker[security])
    
def increment_day(context, data):
    for sec in context.tracker:
        context.tracker[sec].days = context.tracker[sec].days + 1
    
def get_stop_price(price, atr):
    p = price - 2.5 * atr
    return 0 if p < 0 else p
                    
def is_trade(context, data):
    i = 0
    for s in get_open_orders():  
        for o in get_open_orders(s):  
            if o.amount > 0:
                i += 1
    i = len(context.portfolio.positions) + i 
    return False if i > 9 else True
There was a runtime error.