Back to Community
Researching intraday factors with Alphalens: overnight price gap example

The overnight gap strategy (mean reversion of the overnight price gap) is well known and in this example I wanted to quantify the alpha associated with it. The alpha is huge but is that easy to trade it?

Loading notebook preview...
Notebook previews are currently unavailable.
4 responses

Note that I used FixedSlippage, so there is no volume limitation in the orders. This is just for fun, I wanted to see huge numbers.

Clone Algorithm
50
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 quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import morningstar
from quantopian.pipeline import factors, filters, classifiers
from quantopian.pipeline.filters import  StaticAssets, QTradableStocksUS
from quantopian.pipeline.factors import CustomFactor, SimpleMovingAverage, AverageDollarVolume, Returns
 
import pandas as pd
import numpy as np
import scipy.stats as stats
 
 
def make_pipeline(context):    
    universe = QTradableStocksUS()
    pipe = Pipeline()
    pipe.set_screen(universe)
    pipe.add(universe, "universe")
    return pipe
  
def initialize(context):
    set_commission(commission.PerShare(cost=0.00, min_trade_cost=0))
    set_slippage(slippage.FixedSlippage(spread=0.00))
    
    attach_pipeline(make_pipeline(context), 'factors')   
    
    schedule_function(rebalance, date_rules.every_day(), time_rules.market_open())
    schedule_function(close_all, date_rules.every_day(), time_rules.market_close(minutes=270))
    #schedule_function(my_record, date_rules.every_day(), time_rules.market_close())
    
def my_record(context, data):
    record(lever=context.account.leverage,
           exposure=context.account.net_leverage,
           num_pos=len(context.portfolio.positions))
    
def get_weights(pipe_out, rank_cols, max_long_sec, max_short_sec, group_neutral):
    
    if group_neutral:
        pipe_out = pipe_out[rank_cols + ['group']]
    else:
        pipe_out = pipe_out[rank_cols]
    pipe_out = pipe_out.replace([np.inf, -np.inf], np.nan)
    pipe_out = pipe_out.dropna()
 
    def to_weights(factor, is_long_short):
        if is_long_short:
            demeaned_vals = factor - factor.mean()
            return demeaned_vals / demeaned_vals.abs().sum()
        else:
            return factor / factor.abs().sum()
    #
    # rank stocks so that we can select long/short ones
    #
    weights = pd.Series(0., index=pipe_out.index)
           
    for rank_col in rank_cols:
        if not group_neutral: # rank regardless of sector code
            weights += to_weights(pipe_out[rank_col], True)
        else: # weight each sector equally
            weights += pipe_out.groupby(['group'])[rank_col].apply(to_weights, True)
 
    if not group_neutral: # rank regardless of sector/group code
        
        longs  = weights[ weights > 0 ]
        shorts = weights[ weights < 0 ].abs()
        if max_long_sec:
            longs  = longs.order(ascending=False).head(max_long_sec)
        if max_short_sec:
            shorts = shorts.order(ascending=False).head(max_short_sec)
 
    else: # weight each group/sector equally
 
        sectors = pipe_out['group'].unique()
        num_sectors = len(sectors)        
        longs  = pd.Series()
        shorts = pd.Series()
        for current_sector in sectors:
            _w = weights[ pipe_out['group'] == current_sector ]
            _longs  = _w[ _w > 0 ]
            _shorts = _w[ _w < 0 ].abs()
            if max_long_sec:
                _longs  = _longs.order(ascending=False).head(max_long_sec/num_sectors)
            if max_short_sec:
                _shorts = _shorts.order(ascending=False).head(max_short_sec/num_sectors)
            _longs /=  _longs.sum()
            _shorts /= _shorts.sum()
            longs  = longs.append( _longs )                 
            shorts = shorts.append( _shorts )
 
    longs  = longs[ longs > 0 ]
    shorts = shorts[ shorts > 0 ]
    longs  /= longs.sum()
    shorts /= shorts.sum()
        
    return longs, shorts
 
# Compute final rank and assign long and short baskets.
def before_trading_start(context, data):
    results = pipeline_output('factors')
    print 'Basket of stocks %d' % len(results)
    context.universe = results.index
          
def rebalance(context, data):
    
    prices = data.history(context.universe, 'price', 2, '1m')
    
    # compute gap factor
    factor = prices.iloc[:2].pct_change().iloc[1,:] # yesterday close to today open gap
    factor = -factor # mean reverting
    factor.name = 'gap'
    factor = factor.to_frame()
    
    context.longs, context.shorts = get_weights(factor, ['gap'], max_long_sec=150, max_short_sec=150, group_neutral=False)
    context.longs  /= 2
    context.shorts /= 2
        
    print 'longs  weighted (length %d, sum %f):\n' % (len(context.longs.index), context.longs.sum()), context.longs.head(3), context.longs.tail(3)
    print 'shorts weighted (length %d, sum %f):\n' % (len(context.shorts.index), context.shorts.sum()), context.shorts.head(3), context.shorts.tail(3)    

    
    for security in context.shorts.index:
        if get_open_orders(security):
            continue
        if data.can_trade(security):
            order_target_percent(security, -context.shorts[security])
            
    for security in context.longs.index:
        if get_open_orders(security):
            continue
        if data.can_trade(security):
            order_target_percent(security, context.longs[security])
            
    for security in context.portfolio.positions:
        if get_open_orders(security):
            continue
        if data.can_trade(security) and security not in (context.longs.index | context.shorts.index):
            order_target_percent(security, 0)
 
 
def close_all(context, data):
    my_record(context, data)
    
    os = get_open_orders()
    
    for ol in os.values():
        for o in ol:
            cancel_order(o)
    
    for sid in context.portfolio.positions:
        order_target(sid, 0)
There was a runtime error.

@Luca,
Thanks for posting this...always interesting to see your work!

Couldn't resist mucking with this a bit...didn't actually change much, if anything, except for running it for the past two years, which is my pavlovian version of Q-normalization!

Here is my take:
GOOD NEWS:
Fantastic results as measured by cumulative returns!...>49%/2 years

BAD NEWS:
Pretty much ZERO risk factors, except for ~200% turnover each day!

Hmmm. why is this bad you might ask...well bad for me in that now I don't trust that this simulator(backtester) is set up at all to handle intraday trading,
hence, I don't trust the GOOD NEWS of high cumulative returns without a lot more work and validation, especially in the commisions&slippage areas.
I suppose that High Frequency Traders have figured all this out, yet I don't currently trust Zipline in the intraday sphere.

Love to be wrong here...as I really like those returns!
alan

Clone Algorithm
27
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 quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import morningstar
from quantopian.pipeline import factors, filters, classifiers
from quantopian.pipeline.filters import  StaticAssets, QTradableStocksUS
from quantopian.pipeline.factors import CustomFactor, SimpleMovingAverage, AverageDollarVolume, Returns
 
import pandas as pd
import numpy as np
import scipy.stats as stats
 
 
def make_pipeline(context):    
    universe = QTradableStocksUS()
    pipe = Pipeline()
    pipe.set_screen(universe)
    pipe.add(universe, "universe")
    return pipe
  
def initialize(context):
    set_commission(commission.PerShare(cost=0.00, min_trade_cost=0))
    set_slippage(slippage.FixedSlippage(spread=0.00))
    
    attach_pipeline(make_pipeline(context), 'factors')   
    
    schedule_function(rebalance, date_rules.every_day(), time_rules.market_open())
    schedule_function(close_all, date_rules.every_day(), time_rules.market_close(minutes=270))
    #schedule_function(my_record, date_rules.every_day(), time_rules.market_close())
    
def my_record(context, data):
    record(lever=context.account.leverage,
           exposure=context.account.net_leverage,
           num_pos=len(context.portfolio.positions))
    
def get_weights(pipe_out, rank_cols, max_long_sec, max_short_sec, group_neutral):
    
    if group_neutral:
        pipe_out = pipe_out[rank_cols + ['group']]
    else:
        pipe_out = pipe_out[rank_cols]
    pipe_out = pipe_out.replace([np.inf, -np.inf], np.nan)
    pipe_out = pipe_out.dropna()
 
    def to_weights(factor, is_long_short):
        if is_long_short:
            demeaned_vals = factor - factor.mean()
            return demeaned_vals / demeaned_vals.abs().sum()
        else:
            return factor / factor.abs().sum()
    #
    # rank stocks so that we can select long/short ones
    #
    weights = pd.Series(0., index=pipe_out.index)
           
    for rank_col in rank_cols:
        if not group_neutral: # rank regardless of sector code
            weights += to_weights(pipe_out[rank_col], True)
        else: # weight each sector equally
            weights += pipe_out.groupby(['group'])[rank_col].apply(to_weights, True)
 
    if not group_neutral: # rank regardless of sector/group code
        
        longs  = weights[ weights > 0 ]
        shorts = weights[ weights < 0 ].abs()
        if max_long_sec:
            longs  = longs.order(ascending=False).head(max_long_sec)
        if max_short_sec:
            shorts = shorts.order(ascending=False).head(max_short_sec)
 
    else: # weight each group/sector equally
 
        sectors = pipe_out['group'].unique()
        num_sectors = len(sectors)        
        longs  = pd.Series()
        shorts = pd.Series()
        for current_sector in sectors:
            _w = weights[ pipe_out['group'] == current_sector ]
            _longs  = _w[ _w > 0 ]
            _shorts = _w[ _w < 0 ].abs()
            if max_long_sec:
                _longs  = _longs.order(ascending=False).head(max_long_sec/num_sectors)
            if max_short_sec:
                _shorts = _shorts.order(ascending=False).head(max_short_sec/num_sectors)
            _longs /=  _longs.sum()
            _shorts /= _shorts.sum()
            longs  = longs.append( _longs )                 
            shorts = shorts.append( _shorts )
 
    longs  = longs[ longs > 0 ]
    shorts = shorts[ shorts > 0 ]
    longs  /= longs.sum()
    shorts /= shorts.sum()
        
    return longs, shorts
 
# Compute final rank and assign long and short baskets.
def before_trading_start(context, data):
    results = pipeline_output('factors')
    print 'Basket of stocks %d' % len(results)
    context.universe = results.index
          
def rebalance(context, data):
    
    prices = data.history(context.universe, 'price', 2, '1m')
    
    # compute gap factor
    factor = prices.iloc[:2].pct_change().iloc[1,:] # yesterday close to today open gap
    factor = -factor # mean reverting
    factor.name = 'gap'
    factor = factor.to_frame()
    
    context.longs, context.shorts = get_weights(factor, ['gap'], max_long_sec=100, max_short_sec=100, group_neutral=False) #False
    context.longs  /= 2
    context.shorts /= 2
        
    print 'longs  weighted (length %d, sum %f):\n' % (len(context.longs.index), context.longs.sum()), context.longs.head(3), context.longs.tail(3)
    print 'shorts weighted (length %d, sum %f):\n' % (len(context.shorts.index), context.shorts.sum()), context.shorts.head(3), context.shorts.tail(3)    

    
    for security in context.shorts.index:
        if get_open_orders(security):
            continue
        if data.can_trade(security):
            order_target_percent(security, -context.shorts[security])
            
    for security in context.longs.index:
        if get_open_orders(security):
            continue
        if data.can_trade(security):
            order_target_percent(security, context.longs[security])
            
    for security in context.portfolio.positions:
        if get_open_orders(security):
            continue
        if data.can_trade(security) and security not in (context.longs.index | context.shorts.index):
            order_target_percent(security, 0)
 
 
def close_all(context, data):
    my_record(context, data)
    
    os = get_open_orders()
    
    for ol in os.values():
        for o in ol:
            cancel_order(o)
    
    for sid in context.portfolio.positions:
        order_target(sid, 0)
There was a runtime error.

I also really appreciate Luca’s work and for sharing it. However, I also don’t trust these numbers. Too good for such a simple strategy. I don’t think Zipline was really designed with intraday trading in mind, and I believe the risk model doesn’t work unless you have overnight positions.

Luca was playing around, not just 0 slippage, also 0 commissions (useful for testing sometimes).
Comment out the commission & slippage lines: -18%