Back to Community
3 Earnings Example Strategies based on the Quantpedia Trading Strategy Series

The Quantpedia Trading Strategy Series has so far examined three different earnings based trading strategies. As an example of what can be done with the research you find there or anywhere else in the community, Matthew Lee created 3 different variations.

Here's a quick summary of the strategies used in this thread:

  1. Reversals During Earnings Announcements by Nathan Wolfe - Eric C. So of MIT and Sean Wang of UNC show that abnormal short-term returns reversals take place during the period immediately surrounding earnings announcements. They surmise that this reversal results from market makers' response to a temporary demand imbalance, as they temporarily shift the stock's price to ride out the imbalance. (Algorithm + Notebook + Tearsheet)
  2. Are Earnings Predictable with Buyback Announcements? by Seong Lee - The announcement of stock repurchase or secondary equity offering is voluntary and can be easily moved by a few weeks or months. Therefore the timing of SEO or repurchase announcement before earnings announcement could be perceived as important information about future performance of stock during earnings announcement period. (Algorithm + Notebook)
  3. Reversals in the PEAD by Matthew Lee - In his white paper "Overreacting to a History of Underreaction", Milian explores the possibility that well known cross sectional anomalies can reverse over time. Specifically, he investigates the reversal of the PEAD effect. He finds that contrary to previous research, stocks with the most negative previous earnings surprise actually exhibit the most positive returns following the subsequent earnings announcement. (Algorithm + Notebook)

You can find the variations attached to this thread. Copy, clone, and enjoy!

Variation 1

PEAD Reversal + 5 day returns Reversals Rolling Rebalance

Logic:
1. Each day, run a pipeline for stocks, selecting q500 stocks in the top decile of previous earnings surprise (shorts), and lowest decile of previous earnings surprise (longs), and the top quintile of returns, and q1500 stocks in the top decile of 5 day returns (shorts) and lowest decile of 5 day returns (longs).
2. Increment the hold times for each stock held in the portfolio, only keeping stocks for which the hold time < context.days_to_hold
3. Open new long/short positions for stocks generated by the pipeline, equally distributing the portfolio between all long/shorts currently held
4. Close positions for stocks which have been held for > context.days_to_hold

Summary:
This algorithm is an implementation of combined strategy using this paper on the PEAD Reversal in combination with this paper on news driven returns reversals, leveraging the PEAD reversal found after earnings announcements, along with reversals based on 5 day return lookbacks.

*Note that this algorithm uses the LagESurp indicator, rather than the stronger LagEaRet indicator for Earnings Surprise

Clone Algorithm
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 numpy as np
import pandas as pd

from zipline.utils import tradingcalendar
from quantopian.pipeline import CustomFactor, Pipeline
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import AverageDollarVolume, Returns
from quantopian.pipeline.filters.morningstar import Q500US, Q1500US
from quantopian.pipeline.classifiers.morningstar import Sector
from quantopian.pipeline.data.zacks import EarningsSurprises
from quantopian.pipeline.data import morningstar as mstar
from quantopian.pipeline.data.eventvestor import EarningsCalendar
from quantopian.pipeline.data.eventvestor import BuybackAuthorizations
from quantopian.pipeline.factors.eventvestor import (
    BusinessDaysUntilNextEarnings,
    BusinessDaysSincePreviousEarnings,
    BusinessDaysSinceBuybackAuth
)

def initialize(context):
    """
    Called once at the start of the algorithm.
    """   
    context.leverage = 1.0
    context.DAYS_TO_HOLD = {'pead' : 6, 'reversal': 1}
    context.RETURNS_LOOKBACK = 5
    context.MAX_IN_ONE = 1.
    
    context.pead_longs = {}
    context.pead_shorts = {}
  
    schedule_function(rebalance_shorts, date_rules.every_day(), time_rules.market_open())
    schedule_function(rebalance_longs, date_rules.every_day(), time_rules.market_open())
    schedule_function(close_positions, date_rules.every_day(), time_rules.market_open())
    schedule_function(record_vars, date_rules.every_day(), time_rules.market_close())
    attach_pipeline(create_pipeline(context), 'pipe')
                
def create_pipeline(context):    
    # Factors
    lag_e_surp = EarningsSurprises.eps_pct_diff_surp.latest
    adv = AverageDollarVolume(window_length=30, mask=USEquityPricing.volume.latest > 0,)
    returns_quantile = Returns(
            window_length=context.RETURNS_LOOKBACK,
            mask=adv.notnan()
            ).quantiles(5)

    # Filters
    q500 = Q500US()
    q1500 = Q1500US()
    notnan = lag_e_surp.notnan()
    top_dec = lag_e_surp.percentile_between(90, 100, mask=q500)
    bottom_dec = lag_e_surp.percentile_between(0,10, mask=q500)
    has_announcement = BusinessDaysUntilNextEarnings().eq(1)
    adv_top_5 = adv.percentile_between(95, 100)
    mask = has_announcement 

    # Stock picks from PEAD Reversal
    pead_reversal_longs = mask & bottom_dec & notnan & q500
    pead_reversal_shorts = mask & top_dec & notnan & q500
    
    # Stock picks from Earnings Buybacks
    reversal_longs = mask & returns_quantile.eq(0) & adv_top_5 & q1500
    reversal_shorts = mask & returns_quantile.eq(4)  & adv_top_5 & q1500
    
    # Overall Stock Selection
    stocks = pead_reversal_longs | pead_reversal_shorts | reversal_longs | reversal_shorts
    
    return Pipeline(columns= {
                        'pricing': USEquityPricing.close.latest,
                        'pead_reversal_longs': pead_reversal_longs,
                        'pead_reversal_shorts': pead_reversal_shorts,
                        'reversal_longs': reversal_longs,
                        'reversal_shorts': reversal_shorts
                    },
                    screen=stocks)

def before_trading_start(context, data):
    """
    Called every day before market open.
    """
    context.output = pipeline_output('pipe')

    # Currently held PEAD reversal picks get updated and dropped if held for longer than hold time        
    context.pead_longs = {k:v+1 for k,v in context.pead_longs.items() if v < context.DAYS_TO_HOLD['pead']}
    context.pead_shorts = {k:v+1 for k,v in context.pead_shorts.items() if v < context.DAYS_TO_HOLD['pead']}
    
    # Stocks picked by 5 day return reversals are reset (hold time is one day), so not necessary to track time
    context.reversal_longs = []
    context.reversal_shorts = []
    
    # New PEAD reversals positions from our pipeline
    for stock in context.output.index[context.output['pead_reversal_longs'] == True]:
        context.pead_longs[stock] = 0
    
    for stock in context.output.index[context.output['pead_reversal_shorts'] == True]:
        context.pead_shorts[stock] = 0
    
    # New 5 day return reversal positions from our pipeline
    for stock in context.output.index[context.output['reversal_longs'] == True]:
        if stock not in context.pead_longs and stock not in context.pead_shorts:
            context.reversal_longs.append(stock)
            
    for stock in context.output.index[context.output['reversal_shorts'] == True]:
        if stock not in context.pead_longs and stock not in context.pead_shorts:
            context.reversal_shorts.append(stock)
            
    # our aggregate long/short portfolio
    context.aggregate_longs = set(context.pead_longs.keys() + context.reversal_longs)
    context.aggregate_shorts = set(context.pead_shorts.keys() + context.reversal_shorts)
    context.aggregate_stocks = context.aggregate_longs | context.aggregate_shorts
    

def rebalance_shorts(context, data):
    """Manage short positions"""
    short_list = context.aggregate_shorts
    for equity in short_list:
        if data.can_trade(equity):
            order_target_percent(equity, -min(0.5 / len(short_list), context.MAX_IN_ONE))
            
def rebalance_longs(context, data):
    """Manage longs positions"""
    long_list = context.aggregate_longs
    for equity in long_list:
        if data.can_trade(equity):
            order_target_percent(equity, min(0.5 / len(long_list), context.MAX_IN_ONE))

def close_positions(context, data):
    """Close currently held positions which have been held for > hold time"""
    for position in context.portfolio.positions:
        if position not in context.aggregate_stocks:
            order_target_percent(position, 0)
            
def record_vars(context, data):
    """
    Plot variables at the end of each day.
    """
    record(leverage=context.account.leverage)
There was a runtime error.
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.

22 responses

Variation 2

PEAD Reversal + 5 day returns Reversals + Earnings Buybacks Rolling Rebalance

Logic:
1. Each day, run a pipeline for stocks, selecting q500 stocks in the top decile of previous earnings surprise (shorts), and lowest decile of previous earnings surprise (longs), and the top quintile of returns, and q1500 stocks in the top decile of 5 day returns (shorts) and lowest decile of 5 day returns (longs), and buybacks in a [-15, 0] day window before earnings announcements (longs).
2. Increment the hold times for each stock held in the portfolio, only keeping stocks for which the hold time < context.days_to_hold
3. Open new long/short positions for stocks generated by the pipeline, equally distributing the portfolio between all long/shorts currently held
4. Close positions for stocks which have been held for > context.days_to_hold

Summary:
This algorithm is an implementation of combined strategy using this paper on the PEAD Reversal in combination with this paper on news driven returns reversals and this paper on earnings predictability, leveraging the PEAD reversal found after earnings announcements, along with reversals based on 5 day return lookbacks and earnings announcement predictability based on buybacks.

Note that this algorithm uses the LagESurp indicator, rather than the stronger LagEaRet indicator for Earnings Surprise

Clone Algorithm
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 numpy as np
import pandas as pd

from zipline.utils import tradingcalendar
from quantopian.pipeline import CustomFactor, Pipeline
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import AverageDollarVolume, Returns
from quantopian.pipeline.filters.morningstar import Q500US, Q1500US
from quantopian.pipeline.classifiers.morningstar import Sector
from quantopian.pipeline.data.zacks import EarningsSurprises
from quantopian.pipeline.data import morningstar as mstar
from quantopian.pipeline.data.eventvestor import EarningsCalendar
from quantopian.pipeline.data.eventvestor import BuybackAuthorizations
from quantopian.pipeline.factors.eventvestor import (
    BusinessDaysUntilNextEarnings,
    BusinessDaysSincePreviousEarnings,
    BusinessDaysSinceBuybackAuth
)

def initialize(context):
    """
    Called once at the start of the algorithm.
    """   
    context.leverage = 1.0
    context.DAYS_TO_HOLD = {'pead' : 6, 'reversal': 1, 'buyback': 25}
    context.RETURNS_LOOKBACK = 5
    context.MAX_IN_ONE = 1.
    
    context.pead_longs = {}
    context.pead_shorts = {}
    context.buyback_longs = {}
  
    schedule_function(rebalance_shorts, date_rules.every_day(), time_rules.market_open())
    schedule_function(rebalance_longs, date_rules.every_day(), time_rules.market_open())
    schedule_function(close_positions, date_rules.every_day(), time_rules.market_open())
    schedule_function(record_vars, date_rules.every_day(), time_rules.market_close())
    attach_pipeline(create_pipeline(context), 'pipe')
                
def create_pipeline(context):
    # Factors
    lag_e_surp = EarningsSurprises.eps_pct_diff_surp.latest
    adv = AverageDollarVolume(window_length=30, mask=USEquityPricing.volume.latest > 0,)    
    returns_quantile = Returns(
                window_length=context.RETURNS_LOOKBACK,
                mask=adv.notnan()
            ).quantiles(5)
    till_earnings = BusinessDaysUntilNextEarnings()
    since_buybacks = BusinessDaysSinceBuybackAuth()
    since_earnings = BusinessDaysSincePreviousEarnings()
    
    # Filters
    notnan = lag_e_surp.notnan()
    q500 = Q500US()
    q1500 = Q1500US()
    top_dec = lag_e_surp.percentile_between(90, 100, mask=q500)
    bottom_dec = lag_e_surp.percentile_between(0,10, mask=q500)
    has_announcement = BusinessDaysUntilNextEarnings().eq(1)
    adv_top_5 = adv.percentile_between(95,100)
    

    # PEAD Reversal stock picks
    pead_reversal_longs = has_announcement & bottom_dec & notnan & q500
    pead_reversal_shorts = has_announcement & top_dec & notnan & q500
    
    # Returns reversal stock
    reversal_longs = has_announcement & returns_quantile.eq(0) & adv_top_5 & q1500
    reversal_shorts = has_announcement & returns_quantile.eq(4)  & adv_top_5 & q1500
    buyback_longs = ((since_buybacks + till_earnings) <= 15) & q1500
    stocks = pead_reversal_longs | pead_reversal_shorts \
            | reversal_longs | reversal_shorts | buyback_longs
    
    return Pipeline(columns= {
                        'till_earnings': till_earnings,
                        'since_buybacks': since_buybacks,
                        'since_earnings': since_earnings,
                        'market_cap': mstar.valuation.market_cap.latest,
                        'buyback_unit': BuybackAuthorizations.previous_unit.latest,
                        'buyback_amount': BuybackAuthorizations.previous_amount.latest,
                        'buyback_longs': buyback_longs,
                        'pricing': USEquityPricing.close.latest,
                        'pead_reversal_longs': pead_reversal_longs,
                        'pead_reversal_shorts': pead_reversal_shorts,
                        'reversal_longs': reversal_longs,
                        'reversal_shorts': reversal_shorts
                    },
                    screen=stocks)

def convert_units(row):
    """Calculates 'Percent of SO' for each row"""
    buyback_unit = row['buyback_unit']
    market_cap = row['market_cap']
    shares_outstanding = market_cap/row['pricing']
    if buyback_unit == '$M':
        total_bought = row['buyback_amount'] * 1000000.0
        percent_bought = (total_bought)/market_cap
    elif buyback_unit == "Mshares":
        percent_bought = row['buyback_amount']/shares_outstanding
    elif buyback_unit == '%':
        percent_bought = row['buyback_amount']/100.0
    else:
        percent_bought = None
    row['Percent of SO'] = percent_bought
    return row

def before_trading_start(context, data):
    """Called every day before market open"""
    context.output = pipeline_output('pipe')

    # Currently held PEAD reversal picks get updated and dropped if held for longer than hold time        
    context.pead_longs = {k:v+1 for k,v in context.pead_longs.items() if v < context.DAYS_TO_HOLD['pead']}
    context.pead_shorts = {k:v+1 for k,v in context.pead_shorts.items() if v < context.DAYS_TO_HOLD['pead']}
    
    # Stocks picked by 5 day return reversals are reset (hold time is one day)
    context.reversal_longs = []
    context.reversal_shorts = []
    
    # New PEAD reversals positions from our pipeline
    for stock in context.output.index[context.output['pead_reversal_longs'] == True]:
        context.pead_longs[stock] = 0
    
    for stock in context.output.index[context.output['pead_reversal_shorts'] == True]:
        context.pead_shorts[stock] = 0
    
    # New 5 day return reversal positions from our pipeline
    for stock in context.output.index[context.output['reversal_longs'] == True]:
        if stock not in context.pead_longs and stock not in context.pead_shorts:
            context.reversal_longs.append(stock)
            
    for stock in context.output.index[context.output['reversal_shorts'] == True]:
        if stock not in context.pead_longs and stock not in context.pead_shorts:
            context.reversal_shorts.append(stock)
            
    # our aggregate long/short portfolio
    context.aggregate_longs = set(context.pead_longs.keys() + context.reversal_longs)
    context.aggregate_shorts = set(context.pead_shorts.keys() + context.reversal_shorts)
    
    # Stocks picked by buyback get updated and dropped if held for longer than hold time
    context.buyback_longs = {k:v+1 for k,v in context.buyback_longs.items() if \
                             (v < context.DAYS_TO_HOLD['buyback'] and \
                              v not in (context.aggregate_longs | context.aggregate_shorts))}
    
    context.aggregate_stocks = context.aggregate_longs | context.aggregate_shorts \
                               | set(context.buyback_longs.keys())
    
    # Edge case check, since we are doing row operations for new buyback picks
    if len(context.output.index) == 0:
        return

    # Compute 'Percent of SO', for each row
    buybacks = context.output.apply(lambda row: convert_units(row), axis=1)
    
    # Only pick buyback stocks which have 'Percent of SO' > .05
    buybacks = buybacks[buybacks['Percent of SO'] > .05]
    buybacks = buybacks.index[buybacks.buyback_longs]
    
    # New Buyback long picks                                                                            
    for stock in buybacks:
        if stock not in context.aggregate_stocks:
            context.buyback_longs[stock] = 0
            
    context.aggregate_longs = context.aggregate_longs | set(context.buyback_longs.keys())

def rebalance_shorts(context, data):
    """Manage short positions"""
    short_list = context.aggregate_shorts
    for equity in short_list:
        if data.can_trade(equity):
            order_target_percent(equity, -min(0.5 / len(short_list), context.MAX_IN_ONE))
            
def rebalance_longs(context, data):
    """Manage longs positions"""
    long_list = context.aggregate_longs
    for equity in long_list:
        if data.can_trade(equity):
            order_target_percent(equity, min(0.5 / len(long_list), context.MAX_IN_ONE))

def close_positions(context, data):
    for position in context.portfolio.positions:
        if position not in (context.aggregate_longs | context.aggregate_shorts):
            order_target_percent(position, 0)
            
def record_vars(context, data):
    """
    Plot variables at the end of each day.
    """
    record(leverage=context.account.leverage)
There was a runtime error.

Variation 3

PEAD Reversal Rolling Rebalance w/ Bonds

Logic:
1. Each day, run a pipeline for stocks in the Q500, selecting the top decile of previous earnings surprise (shorts), and lowest decile of previous earnings surprise (longs)
2. Increment the hold times for each stock held in the portfolio, only keeping stocks for which the hold time < context.days_to_hold
3. Open new long/short positions for stocks generated by the pipeline, equally distributing the portfolio between all long/shorts currently hold
4. Close positions for stocks which have been held for > context.days_to_hold
5. Fill the leverage gap between the current leverage and desired leverage with long term bonds

Summary:
This algorithm is an implementation of the strategy found in this paper by Milian, leveraging the PEAD reversal found after earnings announcements
This algorithm also utilizes long term bonds to fill the portfolio on days with no earnings announcements.
Note that this algorithm uses the LagESurp indicator, rather than the stronger LagEaRet indicator.

Clone Algorithm
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
"""
PEAD Reversal Strategy w/ Bonds
"""
import numpy as np
import pandas as pd
import math

from zipline.utils import tradingcalendar
from quantopian.pipeline import CustomFactor, Pipeline
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import AverageDollarVolume
from quantopian.pipeline.filters.morningstar import Q500US, Q1500US
from quantopian.pipeline.classifiers.morningstar import Sector
from quantopian.pipeline.data.zacks import EarningsSurprises
from quantopian.pipeline.factors.eventvestor import (
BusinessDaysUntilNextEarnings,
BusinessDaysSincePreviousEarnings
)
from quantopian.pipeline.data.eventvestor import EarningsCalendar

def initialize(context):
    """
    Called once at the start of the algorithm.
    """   
    context.leverage = 1.0
    context.DAYS_TO_HOLD = 6
    context.MAX_DAYS_TO_HOLD = 6
    context.MAX_IN_ONE = 1.
    
    context.bonds = symbols('TLT')
    context.longs = {}
    context.shorts = {}
    
    schedule_function(rebalance, date_rules.every_day(), time_rules.market_open())
    schedule_function(rebalance_bonds, date_rules.every_day(), time_rules.market_open(hours=2))
    schedule_function(record_vars, date_rules.every_day(), time_rules.market_close())

    attach_pipeline(create_pipeline(Q500US()), 'pipe')
    
def rebalance_bonds(context, data):
    """Fill portfolio with bonds when current leverage < context.leverage"""
    P = context.portfolio    
    market_value = sum(data.current(i, 'price') * abs(P.positions[i].amount) for i in context.portfolio.positions)
    leverage = market_value / max(1, P.portfolio_value)
    gap = max(context.leverage - leverage, 0.0)
    weight = gap / len(context.bonds)
    record(bond_pct=gap*100)
    for bnd in context.bonds:
        if not math.isnan(weight):
            order_target_percent(bnd, weight)
                
def create_pipeline(mask):
    # Earnings Surprise Factor from previous quarter
    lag_e_surp = EarningsSurprises.eps_pct_diff_surp.latest
    
    # Filters 
    top_dec = lag_e_surp.percentile_between(90, 100, mask=mask)
    bottom_dec = lag_e_surp.percentile_between(0,10, mask=mask)
    has_announcement = BusinessDaysUntilNextEarnings().eq(1)
    mask = has_announcement & mask & (top_dec | bottom_dec)
    
    return Pipeline(columns= {
            
                        'LagESurp': lag_e_surp,
                        'top_dec': top_dec,
                        'bottom_dec': bottom_dec
                    },
                    screen=mask)
 
def before_trading_start(context, data):
    """
    Called every day before market open.
    """
    context.output = pipeline_output('pipe')
    
    # Discard stocks which have been held for > Hold Period
    context.longs = {k:v+1 for k,v in context.longs.items() if v < context.DAYS_TO_HOLD}
    context.shorts = {k:v+1 for k,v in context.shorts.items() if v < context.DAYS_TO_HOLD}
    
    # Add new stocks which pass our pipeline filter
    for stock in context.output.index[context.output['bottom_dec'] == True]:
        context.longs[stock] = 0
    
    for stock in context.output.index[context.output['top_dec'] == True]:
        context.shorts[stock] = 0

def rebalance(context, data):
    short_list = context.shorts
    long_list = context.longs
    
    # Rebalance our shorts, weighted equally
    for equity in short_list:
        if data.can_trade(equity):
            order_target_percent(equity, -min(0.3 / len(short_list), context.MAX_IN_ONE))
            
    # Rebalance our longs, weighted equally
    for equity in long_list:
        if data.can_trade(equity):
            order_target_percent(equity, min(0.7 / len(long_list), context.MAX_IN_ONE))

    # Close positions which have been held for > Hold Period
    for position in context.portfolio.positions:
        if position not in context.shorts.keys() + context.longs.keys():
            order_target_percent(position, 0)
            
            
def record_vars(context, data):
    """
    Plot variables at the end of each day.
    """
    record(leverage=context.account.leverage)
There was a runtime error.

Hi Seong Lee,

I'm seeing some order partially filled error while backtesting the above algorithm.
I'm wandering if you see the same error as well?

2008-11-29 02:00 WARN Your order for -30376 shares of INTU has been partially filled. 28495 shares were successfully sold. 1881 shares were not filled by the end of day and were canceled.  
2008-12-25 02:00 WARN Your order for 7664 shares of TLT has been partially filled. 7313 shares were successfully purchased. 351 shares were not filled by the end of day and were canceled.  
2008-12-27 05:00 WARN Your order for 6121 shares of TLT has been partially filled. 3789 shares were successfully purchased. 2332 shares were not filled by the end of day and were canceled.  
2010-10-05 04:00 WARN Your order for -57213 shares of KBH has been partially filled. 53612 shares were successfully sold. 3601 shares were not filled by the end of day and were canceled.  
2012-09-06 04:00 WARN Your order for 73207 shares of NAV has been partially filled. 63267 shares were successfully purchased. 9940 shares were not filled by the end of day and were canceled.  
2012-09-15 04:00 WARN Your order for -66839 shares of NAV has been partially filled. 39084 shares were successfully sold. 27755 shares were not filled by the end of day and were canceled.  
2012-09-18 04:00 WARN Your order for -27755 shares of NAV has been partially filled. 26323 shares were successfully sold. 1432 shares were not filled by the end of day and were canceled.  
2013-04-02 04:00 WARN Your order for -81080 shares of KBH has been partially filled. 79588 shares were successfully sold. 1492 shares were not filled by the end of day and were canceled.  
2013-06-12 04:00 WARN Your order for 64376 shares of HRB has been partially filled. 49986 shares were successfully purchased. 14390 shares were not filled by the end of day and were canceled.  
2013-09-14 04:00 WARN Your order for -87701 shares of PAY has been partially filled. 51093 shares were successfully sold. 36608 shares were not filled by the end of day and were canceled.  
2013-09-17 04:00 WARN Your order for -36608 shares of PAY has been partially filled. 32371 shares were successfully sold. 4237 shares were not filled by the end of day and were canceled.  

See Choon

Hi See Choon,

Doesn't seem like those are errors, but warnings that your orders were partially filled.

Seong

Hi See Choon,

It doesn't seem like those are errors. Those are warnings that your orders have been partially filled.

I do believe that the algos attempt to exit these positions until you no longer have positions in them, see the code snippet below that's been taken from the strategies above (and you can find similar ones in the others if this exact one isn't there):

    # Close positions which have been held for > Hold Period  
    for position in context.portfolio.positions:  
        if position not in context.shorts.keys() + context.longs.keys():  
            order_target_percent(position, 0)  

Backtested the PEAD Reversal Rolling Rebalance w/ Bonds

NoDataAvailable: Backtest began on 2011-01-04 and ended on 2016-11-18, but some of the requested datasets do not have data for this time. The datasets are: eventvestor.earnings_calendar_free: start=2007-01-01, end=2014-12-04. Your backtest must begin on or after: 2007-01-01 and end on or before: 2014-12-04.
There was a runtime error on line 75.

Hi Kim,
Premium datasets provide a free sample period and a time period for which you need to subscribe. In the case of the earnings calendar from EventVestor, you have free access up to two years ago (2014-12-04 as indicated by the error message). To run a backtest for the time period up to 2016-11-18 as you attempted, you'd need to subscribe to the data set at http://quantopian.com/data/eventvestor/earnings_calendar

Thanks
Josh

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.

In regards to the paper, "Overreacting to a History of Underreaction", I am also finding similar results using an event study. Yet, I would like to ask the question on the proper way to do the events study. The way I have done it is by aggregating through 2 dimensions: cross sectional aggregation and time aggregation. For example:

"-1 days"   "0 days"    "+1 days"  

firm 0 -1.0% 10.0% 2.0%
firm 1 0.0 -5.0 -1.0
firm 2 0.0 12.0 3.0
firm n 2.0 -6.0 -2.0
AAR 0.3 2.8 0.5
CAAR =3.5 %
Day 0 is the earnings announcement date.

The abnormal return (AR) is averaged per day across the firms and the CAAR is the sum of the ARs across the 3 days. Does this methodology agree with the group on event studies? If so, I also see a drift, yet to prove if statistically significant, but if earnings surprise is negative and abnormal returns around [-1,0,1] is negative that on average after 5 days the abnormal returns start to drift up. Curious what people think on this?

Hi Jose, I haven't dug into your questions specifically but you might might find this thread useful: https://www.quantopian.com/posts/cloning-available-how-to-conduct-your-own-event-study-using-research

It has a good template for an event study written by Seong, with improvements from our community.

Hi Seong Lee, thanks for sharing!

How are the returns of the 1st variation (PEAD Reversal + 5 day returns Reversals Rolling Rebalance) in the last years (2014-05 to 2016-12)?

Thanks,

@martin

Clone Algorithm
22
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 numpy as np
import pandas as pd

from zipline.utils import tradingcalendar
from quantopian.pipeline import CustomFactor, Pipeline
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import AverageDollarVolume, Returns
from quantopian.pipeline.filters.morningstar import Q500US, Q1500US
from quantopian.pipeline.classifiers.morningstar import Sector
from quantopian.pipeline.data.zacks import EarningsSurprises
from quantopian.pipeline.data import morningstar as mstar
from quantopian.pipeline.data.eventvestor import EarningsCalendar
from quantopian.pipeline.data.eventvestor import BuybackAuthorizations
from quantopian.pipeline.factors.eventvestor import (
    BusinessDaysUntilNextEarnings,
    BusinessDaysSincePreviousEarnings,
    BusinessDaysSinceBuybackAuth
)

def initialize(context):
    """
    Called once at the start of the algorithm.
    """   
    context.leverage = 1.0
    context.DAYS_TO_HOLD = {'pead' : 6, 'reversal': 1}
    context.RETURNS_LOOKBACK = 5
    context.MAX_IN_ONE = 1.
    
    context.pead_longs = {}
    context.pead_shorts = {}
  
    schedule_function(rebalance_shorts, date_rules.every_day(), time_rules.market_open())
    schedule_function(rebalance_longs, date_rules.every_day(), time_rules.market_open())
    schedule_function(close_positions, date_rules.every_day(), time_rules.market_open())
    schedule_function(record_vars, date_rules.every_day(), time_rules.market_close())
    attach_pipeline(create_pipeline(context), 'pipe')
                
def create_pipeline(context):    
    # Factors
    lag_e_surp = EarningsSurprises.eps_pct_diff_surp.latest
    adv = AverageDollarVolume(window_length=30, mask=USEquityPricing.volume.latest > 0,)
    returns_quantile = Returns(
            window_length=context.RETURNS_LOOKBACK,
            mask=adv.notnan()
            ).quantiles(5)

    # Filters
    q500 = Q500US()
    q1500 = Q1500US()
    notnan = lag_e_surp.notnan()
    top_dec = lag_e_surp.percentile_between(90, 100, mask=q500)
    bottom_dec = lag_e_surp.percentile_between(0,10, mask=q500)
    has_announcement = BusinessDaysUntilNextEarnings().eq(1)
    adv_top_5 = adv.percentile_between(95, 100)
    mask = has_announcement 

    # Stock picks from PEAD Reversal
    pead_reversal_longs = mask & bottom_dec & notnan & q500
    pead_reversal_shorts = mask & top_dec & notnan & q500
    
    # Stock picks from Earnings Buybacks
    reversal_longs = mask & returns_quantile.eq(0) & adv_top_5 & q1500
    reversal_shorts = mask & returns_quantile.eq(4)  & adv_top_5 & q1500
    
    # Overall Stock Selection
    stocks = pead_reversal_longs | pead_reversal_shorts | reversal_longs | reversal_shorts
    
    return Pipeline(columns= {
                        'pricing': USEquityPricing.close.latest,
                        'pead_reversal_longs': pead_reversal_longs,
                        'pead_reversal_shorts': pead_reversal_shorts,
                        'reversal_longs': reversal_longs,
                        'reversal_shorts': reversal_shorts
                    },
                    screen=stocks)

def before_trading_start(context, data):
    """
    Called every day before market open.
    """
    context.output = pipeline_output('pipe')

    # Currently held PEAD reversal picks get updated and dropped if held for longer than hold time        
    context.pead_longs = {k:v+1 for k,v in context.pead_longs.items() if v < context.DAYS_TO_HOLD['pead']}
    context.pead_shorts = {k:v+1 for k,v in context.pead_shorts.items() if v < context.DAYS_TO_HOLD['pead']}
    
    # Stocks picked by 5 day return reversals are reset (hold time is one day), so not necessary to track time
    context.reversal_longs = []
    context.reversal_shorts = []
    
    # New PEAD reversals positions from our pipeline
    for stock in context.output.index[context.output['pead_reversal_longs'] == True]:
        context.pead_longs[stock] = 0
    
    for stock in context.output.index[context.output['pead_reversal_shorts'] == True]:
        context.pead_shorts[stock] = 0
    
    # New 5 day return reversal positions from our pipeline
    for stock in context.output.index[context.output['reversal_longs'] == True]:
        if stock not in context.pead_longs and stock not in context.pead_shorts:
            context.reversal_longs.append(stock)
            
    for stock in context.output.index[context.output['reversal_shorts'] == True]:
        if stock not in context.pead_longs and stock not in context.pead_shorts:
            context.reversal_shorts.append(stock)
            
    # our aggregate long/short portfolio
    context.aggregate_longs = set(context.pead_longs.keys() + context.reversal_longs)
    context.aggregate_shorts = set(context.pead_shorts.keys() + context.reversal_shorts)
    context.aggregate_stocks = context.aggregate_longs | context.aggregate_shorts
    

def rebalance_shorts(context, data):
    """Manage short positions"""
    short_list = context.aggregate_shorts
    for equity in short_list:
        if data.can_trade(equity):
            order_target_percent(equity, -min(0.5 / len(short_list), context.MAX_IN_ONE))
            
def rebalance_longs(context, data):
    """Manage longs positions"""
    long_list = context.aggregate_longs
    for equity in long_list:
        if data.can_trade(equity):
            order_target_percent(equity, min(0.5 / len(long_list), context.MAX_IN_ONE))

def close_positions(context, data):
    """Close currently held positions which have been held for > hold time"""
    for position in context.portfolio.positions:
        if position not in context.aggregate_stocks:
            order_target_percent(position, 0)
            
def record_vars(context, data):
    """
    Plot variables at the end of each day.
    """
    record(leverage=context.account.leverage)
There was a runtime error.
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.

hi, wondering if you can help on events study. Create events window as earnings announcement date +/- extra days. As for as aggregating, I group the firms that have earnings on the same date and calculate abnormal returns say event T. Yet, if the next earnings date is T+1, again, I group the firms that have earnings on the same date and calculate abnormal returns. Yet, if I wanted to get the average abnormal returns, how would I combine these two groups as one to see the event study? The two event windows overlap and combing them I feel would be incorrect? Any suggestions would be useful. thanks

great

In case anyone gets tempted by the smooth 'up and to the right' curve of Variation 1, here is a more recent backtest of the exact same algo.

Clone Algorithm
18
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 numpy as np
import pandas as pd

from zipline.utils import tradingcalendar
from quantopian.pipeline import CustomFactor, Pipeline
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import AverageDollarVolume, Returns
from quantopian.pipeline.filters.morningstar import Q500US, Q1500US
from quantopian.pipeline.classifiers.morningstar import Sector
from quantopian.pipeline.data.zacks import EarningsSurprises
from quantopian.pipeline.data import morningstar as mstar
from quantopian.pipeline.data.eventvestor import EarningsCalendar
from quantopian.pipeline.data.eventvestor import BuybackAuthorizations
from quantopian.pipeline.factors.eventvestor import (
    BusinessDaysUntilNextEarnings,
    BusinessDaysSincePreviousEarnings,
    BusinessDaysSinceBuybackAuth
)

def initialize(context):
    """
    Called once at the start of the algorithm.
    """   
    context.leverage = 1.0
    context.DAYS_TO_HOLD = {'pead' : 6, 'reversal': 1}
    context.RETURNS_LOOKBACK = 5
    context.MAX_IN_ONE = 1.
    
    context.pead_longs = {}
    context.pead_shorts = {}
  
    schedule_function(rebalance_shorts, date_rules.every_day(), time_rules.market_open())
    schedule_function(rebalance_longs, date_rules.every_day(), time_rules.market_open())
    schedule_function(close_positions, date_rules.every_day(), time_rules.market_open())
    schedule_function(record_vars, date_rules.every_day(), time_rules.market_close())
    attach_pipeline(create_pipeline(context), 'pipe')
                
def create_pipeline(context):    
    # Factors
    lag_e_surp = EarningsSurprises.eps_pct_diff_surp.latest
    adv = AverageDollarVolume(window_length=30, mask=USEquityPricing.volume.latest > 0,)
    returns_quantile = Returns(
            window_length=context.RETURNS_LOOKBACK,
            mask=adv.notnan()
            ).quantiles(5)

    # Filters
    q500 = Q500US()
    q1500 = Q1500US()
    notnan = lag_e_surp.notnan()
    top_dec = lag_e_surp.percentile_between(90, 100, mask=q500)
    bottom_dec = lag_e_surp.percentile_between(0,10, mask=q500)
    has_announcement = BusinessDaysUntilNextEarnings().eq(1)
    adv_top_5 = adv.percentile_between(95, 100)
    mask = has_announcement 

    # Stock picks from PEAD Reversal
    pead_reversal_longs = mask & bottom_dec & notnan & q500
    pead_reversal_shorts = mask & top_dec & notnan & q500
    
    # Stock picks from Earnings Buybacks
    reversal_longs = mask & returns_quantile.eq(0) & adv_top_5 & q1500
    reversal_shorts = mask & returns_quantile.eq(4)  & adv_top_5 & q1500
    
    # Overall Stock Selection
    stocks = pead_reversal_longs | pead_reversal_shorts | reversal_longs | reversal_shorts
    
    return Pipeline(columns= {
                        'pricing': USEquityPricing.close.latest,
                        'pead_reversal_longs': pead_reversal_longs,
                        'pead_reversal_shorts': pead_reversal_shorts,
                        'reversal_longs': reversal_longs,
                        'reversal_shorts': reversal_shorts
                    },
                    screen=stocks)

def before_trading_start(context, data):
    """
    Called every day before market open.
    """
    context.output = pipeline_output('pipe')

    # Currently held PEAD reversal picks get updated and dropped if held for longer than hold time        
    context.pead_longs = {k:v+1 for k,v in context.pead_longs.items() if v < context.DAYS_TO_HOLD['pead']}
    context.pead_shorts = {k:v+1 for k,v in context.pead_shorts.items() if v < context.DAYS_TO_HOLD['pead']}
    
    # Stocks picked by 5 day return reversals are reset (hold time is one day), so not necessary to track time
    context.reversal_longs = []
    context.reversal_shorts = []
    
    # New PEAD reversals positions from our pipeline
    for stock in context.output.index[context.output['pead_reversal_longs'] == True]:
        context.pead_longs[stock] = 0
    
    for stock in context.output.index[context.output['pead_reversal_shorts'] == True]:
        context.pead_shorts[stock] = 0
    
    # New 5 day return reversal positions from our pipeline
    for stock in context.output.index[context.output['reversal_longs'] == True]:
        if stock not in context.pead_longs and stock not in context.pead_shorts:
            context.reversal_longs.append(stock)
            
    for stock in context.output.index[context.output['reversal_shorts'] == True]:
        if stock not in context.pead_longs and stock not in context.pead_shorts:
            context.reversal_shorts.append(stock)
            
    # our aggregate long/short portfolio
    context.aggregate_longs = set(context.pead_longs.keys() + context.reversal_longs)
    context.aggregate_shorts = set(context.pead_shorts.keys() + context.reversal_shorts)
    context.aggregate_stocks = context.aggregate_longs | context.aggregate_shorts
    

def rebalance_shorts(context, data):
    """Manage short positions"""
    short_list = context.aggregate_shorts
    for equity in short_list:
        if data.can_trade(equity):
            order_target_percent(equity, -min(0.5 / len(short_list), context.MAX_IN_ONE))
            
def rebalance_longs(context, data):
    """Manage longs positions"""
    long_list = context.aggregate_longs
    for equity in long_list:
        if data.can_trade(equity):
            order_target_percent(equity, min(0.5 / len(long_list), context.MAX_IN_ONE))

def close_positions(context, data):
    """Close currently held positions which have been held for > hold time"""
    for position in context.portfolio.positions:
        if position not in context.aggregate_stocks:
            order_target_percent(position, 0)
            
def record_vars(context, data):
    """
    Plot variables at the end of each day.
    """
    record(leverage=context.account.leverage)
There was a runtime error.

I do not have any live algo trading experience, and that's why I come up with this question. I have 20k in my IB account. I want to revise the above algorithm (say Variation 1) in the post for live trading, here are two questions:
(1) what data shall I subscribe from quantopian at minimum to save my cost? (2) If I have not ordered data used in code, does Quantopian platform give a warning when I try to launch it for live trading?

I guess it shall be no problem if I order "pipeline data bundle", which is too expensive for me. I saw the code importing data from Zacks, and Eventvestor. But, I also do not know which Eventvestor service is particularly needed in this code. Thanks.

Looks like Variation 1 uses:

https://www.quantopian.com/data/zacks/earnings_surprises
https://www.quantopian.com/data/eventvestor/earnings_calendar
https://www.quantopian.com/data/eventvestor/buyback_auth

Would it be possible to use this on Robinhood in some way since you can only hold long positions on Robinhood?

was trying to backtest variation 3 posted here, but it just throws an error when building, and doesn't provide a line number or anything, can anyone fix it?

To narrow it down, remove has_announcement, it then ran ok for me.

yeah that throws an error i think i figured it out, i think you need to subscribe to earnings data

Thanks for sharing! yet this idea suffers recently and did not manage to escape the 08 crisis either

Great strategies. Thank you for sharing.