Back to Community

3 responses

I wonder if the asymmetry is due to this line

filtered_uni = context.output[context.output['high_to_open_returns'] < context.output['std']]  

Generally yesterday's high will be greater than today's open, so high_to_open_returns will generally be negative, and therefore always be below the standard deviation.

Using the last (corrected version) and changing the entry time to 10 minutes after the open and the exit time to 10 minutes before the close,
(they were 0 and 30 minutes) the results are quite bad. It makes me worry when results are really sensitive to small changes like this. It does seem to be selling everything at the end of day.

Clone Algorithm
Backtest from to with initial capital
Total Returns
Max Drawdown
Benchmark Returns
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
Quantopian Algo by:
Derek M Tishler - [email protected]

The basis for this strategy comes from Example 4.1 on pg 93+ in the book:
Algorithmic Trading, Winning Strategies and their Rationale by Ernest P. Chan
Please support the author and also check out his blog!

I attempt to recreate Example 4.1's Buy-On-Gap strategy using the
Q1500US in place of the survivorship biased S&P500. Both the Long and Short
versions are implemented to form a hedged portfolio for intraday mean reversion.

Please see the book for a full explanation, but to summarize the model:
1. Select stocks near market open with gaps(returns) from previous days low/high to today's open which are lower than one standard deviation.
2. Filter stocks to be above/below their moving averages.
3. Build a portfolio with the lowest returns(see 1).(Should short side be conversely "highest"?)
4. Firesale portfolio at close.
note: The book uses a massless, frictionless strategy with no commissions or slippage. Here I explore a slightly more pragmatic strategy in steps.

Exiting all positions at market-close is difficult. Eventually, some positions will be held overnight. This problem is combined with added signal noise(pg 95) from working post market-open and well before market-close. This contributes to a deviation from the model's intent and introduces a diminishing capital capacity(visible in no-commission backtest where end-of-day leverage no longer goes to zero).

The results are not consistently decent with(see next post) or without commission. Perhaps risk management can retain enough returns to turn things around:
There is already some unimplemented risk management at the end of the code that
can hopefully be tuned to fix the cumulative returns when commissions are on(2009 to 2015 stall). Just un comment the handle_data function and add your percent value(Like 0.05 for 5%) to add one of four
RMs(long profit take, long stop loss, short profit take, short stop loss).

from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from import USEquityPricing
from quantopian.pipeline.factors import SimpleMovingAverage, Latest
from quantopian.pipeline import CustomFactor
from quantopian.pipeline.filters.morningstar import Q1500US

import numpy as np

class STD(CustomFactor):    
    inputs        = [USEquityPricing.close]  
    window_length = 90
    # compute standard deviation  
    def compute(self, today, assets, out, close):  
        out[:] = np.std(np.diff(close[-self.window_length:], axis=0), axis=0)

def initialize(context):
    # Max number of positions per side of portfolio(long/short)
    context.n_positions_at_L_1 = 10
    # Intraday leverage targets, intention is to be 0.0 overnight(ideal)
    context.long_leverage      = 1.0
    context.short_leverage     = 1.0
    # For testing massless, frictionless strategies.
    #set_commission(commission.PerShare(cost=0.0, min_trade_cost=0.0))
    # Lets say super low comissions with some hidden fees
    #set_commission(commission.PerShare(cost=0.0040, min_trade_cost=0.40))
    # Enter/Exit positions at market open
    schedule_function(enter_positions, date_rules.every_day(), time_rules.market_open(minutes=10))
    # Firesale at end of day
    schedule_function(exit_positions, date_rules.every_day(), time_rules.market_close(minutes=10))

    # Record leverage
    schedule_function(my_record_open, date_rules.every_day(), time_rules.market_open())
    schedule_function(my_record_mid, date_rules.every_day(), time_rules.market_open(hours=3, minutes=2))
    schedule_function(my_record_close, date_rules.every_day(), time_rules.market_close())
    # Create a stock "selector" to generate daily, unbiased universe+metrics
    attach_pipeline(make_pipeline(), 'buy_on_gap')
def make_pipeline():

    # Instead of survivorship biased s&p500 like in the book, let's use the Q1500US
    base_universe = Q1500US()
    ## Gather these items to use against open price calulations each morning
    # Universes std using close-close over last 90 days
    std = STD()
    # 20 day moving average of the close price
    ma  = SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=20)
    # previous day's low & high to define long/short gaps
    low  = Latest(inputs=[USEquityPricing.low], window_length=1)
    high = Latest(inputs=[USEquityPricing.high], window_length=1)
    pipe = Pipeline(
        screen = base_universe,
        columns = {
            'std': std,
            'ma': ma,
            'low': low,
            'high': high
    return pipe
def before_trading_start(context, data):
    context.output = pipeline_output('buy_on_gap')
    context.security_list = context.output.index
def my_record_open(context, data):
def my_record_mid(context, data):
def my_record_close(context, data):
def enter_positions(context,data):
    # Now that we are within a trading day, we can access an updated open price
    todays_open = data.history(context.security_list, 'open', 1, '1d')
    # The current day's open price
    context.output['open'] = todays_open.values.flatten()
    # Previous-day's-low to today's-open returns
    context.output['low_to_open_returns']  = context.output['open'] - context.output['low']
    context.output['high_to_open_returns'] = context.output['open'] - context.output['high']
    ## Perform LONG Strategy rank based on pg 93 ex 4.1
    # select stocks whos retuns from their previous days lows to today open are lower than one standard deviation(down gappers have negative returns so negative 1*std filter)
    filtered_uni = context.output[context.output['low_to_open_returns'] < -context.output['std']]
    # Require open price to be above than 20 day moving average of the close
    filter_ma    = filtered_uni['open'] > filtered_uni['ma']
    # Buy top 10 stocks with lowest returns from their previous day's lows
    longs = filtered_uni[filter_ma].sort_values(['low_to_open_returns']).tail(context.n_positions_at_L_1)
    # Positions printout
    #print "Today's Longs:\n"+ ", ".join([stock.symbol for stock in longs.index.values])
    # Enter long positions
    for stock in longs.index.values:
        if data.can_trade(stock):
    ## Perform SHORT Strategy rank based on pg 95-96
    # select stocks whos retuns from their previous days high to today's open are higher than one standard deviation(Up gappers)
    filtered_uni = context.output[context.output['high_to_open_returns'] > context.output['std']]
    # Require open price to be below than 20 day moving average of the close
    filter_ma    = filtered_uni['open'] < filtered_uni['ma']
    # Buy top 10 stocks with the highest returns from their previous day's high
    shorts = filtered_uni[filter_ma].sort_values(['high_to_open_returns']).head(context.n_positions_at_L_1)
    # Positions printout
    #print "Today's Shorts:\n"+ ", ".join([stock.symbol for stock in shorts.index.values])
    # Enter short positions
    for stock in shorts.index.values:
        if data.can_trade(stock):
    # Exit positions leftover, from slippage, from previous close. Ideally not#0.15hing held overnight.
    for stock in context.portfolio.positions:
        if (stock not in longs.index.values) and (stock not in shorts.index.values):
            order_target_percent(stock, 0.0)

def exit_positions(context,data):
    # liquidate at market close
    for stock in context.portfolio.positions:
        order_target_percent(stock, 0.0)

## If you want to play with simple RM, you can start here.        
#def handle_data(context, data):
#    check_positions_for_loss_or_profit(context, data)

# Taken from Quantopian Help Page and slightly edited for testing item by item.
def check_positions_for_loss_or_profit(context, data):
    # Set a value to activate risk management type
    profit_take_long  = None
    stop_loss_long    = None
    profit_take_short = None
    stop_loss_short   = None
    # Sell our positions on longs/shorts for profit or loss
    context.stocks_held = context.portfolio.positions
    for security in context.portfolio.positions:
        is_stock_held = context.stocks_held.get(security) >= 0
        if data.can_trade(security) and is_stock_held and not get_open_orders(security):
            current_position = context.portfolio.positions[security].amount  
            cost_basis = context.portfolio.positions[security].cost_basis  
            price = data.current(security, 'price')
            # On Long & Profit
            if profit_take_long is not None:
                if price >= cost_basis * (1.0+profit_take_long) and current_position > 0:  
                    order_target_percent(security, 0)  
           str(security) + ' Sold Long for Profit')  
            # On Short & Profit
            if profit_take_short is not None:
                if price <= cost_basis* (1.0-profit_take_short) and current_position < 0:
                    order_target_percent(security, 0)  
           str(security) + ' Sold Short for Profit')  
            # On Long & Loss
            if stop_loss_long is not None:
                if price <= cost_basis * (1.0-stop_loss_long) and current_position > 0:  
                    order_target_percent(security, 0)  
           str(security) + ' Sold Long for Loss')  
            # On Short & Loss
            if stop_loss_short is not None:
                if price >= cost_basis * (1.0+stop_loss_short) and current_position < 0:  
                    order_target_percent(security, 0)  
           str(security) + ' Sold Short for Loss')  
There was a runtime error.

I would like to add an adjustable volume filter and a price filter. I’ve come up with the following code, but do not know how to implement it within the existing code so that the algo filters out the securities that do not meet these multiple minimum criteria. Can someone please help with this?

twodayvol = data.history(context.security_list, ‘price’, 20, ‘1m’)
yesterday_vol = twodayvol[-20:-11].mean()
today_vol = twodayvol[-9:-1].mean()
vol_jump = yesterday_vol *2
context.output[‘todayvol’] = today_vol.values
context.output[‘voljump’] = vol_jump.values
filtered_vol = context.output[‘todayvol’] > context.output[‘voljump’]
filtered_price = context.output[‘open’] > 30.0