Back to Community
How to use Earnings Announcements in your strategies

Earnings announcements happen every quarter every year and tend to correlate to volatility for individual stocks. If the economic hypothesis behind your algorithm doesn’t specifically rely on price movements related to earnings announcements, one possible way to lower volatility is to avoid securities that are close to an earnings announcement.

Thankfully, there are a two ways for you to do this:

  1. Import your own earnings calendar dataset through Fetcher()
  2. See the sample algorithm attached which uses EventVestor's Earnings Calendar dataset

This algorithm, originally published by James Christopher and Delaney, uses two new Pipeline API factors (BusinessDaysUntilNextEarnings and BusinessDaysSincePreviousEarnings) to avoid stocks which have an earnings announcement 7 days ahead and 7 days previous. You can clone the algorithm to see it in action.

When BusinessDaysUntilNextEarnings == 0 it means that the current day has an earnings announcement. Either before market open or after market close.

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.

31 responses

I know this is just an example, but is it intentional that you are avoiding earnings +/- 3 days, yet only rebalancing once a month? I would have thought that might mean you'd want +/- 22 business days or something like that, to ensure that you don't have earnings for any stocks until your next rebalance. That, or update the rebalancing frequency to daily or weekly.

Also, for whatever reason, the earnings-augmented algo seems to have substantially higher periodic net exposure, which looks like a bug, if the algo is intended to be market neutral.

Good point. Like you mentioned this is an example and the monthly rebalancing frequency was left in to try and preserve the original algorithm as much as possible.

But I see where you're coming from and thanks for bringing up the point. Would definitely encourage others to try playing around with the rebalance frequencies and seeing the results! As for the returns I originally thought that it was because we had avoided a negative earnings surprise but it also may just be a bug like you mentioned.

@Simon: regarding the higher net exposure you can have a look at initialize function, there is a comment:

here are two options: Screen your securities out in initialize() or do it right before you order.  

The backtest shown in this thread uses option 2. That means the screening is done right before ordering stocks, when weights have been already assigned. The problem is that some stocks are not going to be ordered due to earning announcements, making the calculated weights wrong and eventually increasing net exposure. So go for option 1 to fix this (that is uncomment the code for stock screeening in initialize)

Yeah I am trying, working through some bugs.

If you don't mind my little changes, here is a version with slightly better net exposure. Unfortunately it is difficult to get better results with monthly rebalancing

Clone Algorithm
89
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.factors import CustomFactor, SimpleMovingAverage
from quantopian.pipeline.data import morningstar

# For full information on the dataset please visit Quantopian data at
# https://www.quantopian.com/data/eventvestor/earnings_calendar
# Both Free & Paid versions will be accessed through the same
# namespace
from quantopian.pipeline.data.eventvestor import EarningsCalendar
from quantopian.pipeline.data.eventvestor.factors import BusinessDaysUntilNextEarnings, BusinessDaysSincePreviousEarnings

import pandas as pd
import numpy as np


class AvgDailyDollarVolumeTraded(CustomFactor):
    
    inputs = [USEquityPricing.close, USEquityPricing.volume]
    window_length = 20
    
    def compute(self, today, assets, out, close_price, volume):
        out[:] = np.mean(close_price * volume, axis=0)
        

class Value(CustomFactor):
    
    inputs = [morningstar.valuation_ratios.book_value_yield,
              morningstar.valuation_ratios.sales_yield,
              morningstar.valuation_ratios.fcf_yield] 
    
    window_length = 1
    
    def compute(self, today, assets, out, book_value, sales, fcf):
        value_table = pd.DataFrame(index=assets)
        value_table["book_value"] = book_value[-1]
        value_table["sales"] = sales[-1]
        value_table["fcf"] = fcf[-1]
        out[:] = value_table.rank().mean(axis=1)
    
    
class Momentum(CustomFactor):
    
    inputs = [USEquityPricing.close]
    window_length = 252
    
    def compute(self, today, assets, out, close):       
        out[:] = close[-20] / close[0]

            
class Quality(CustomFactor):
    
    inputs = [morningstar.operation_ratios.roe]
    window_length = 1
    
    def compute(self, today, assets, out, roe):       
        out[:] = roe[-1]
        
        
class Volatility(CustomFactor):
    
    inputs = [USEquityPricing.close]
    window_length = 252
    
    def compute(self, today, assets, out, close):  
        close = pd.DataFrame(data=close, columns=assets) 
        # Since we are going to rank largest is best we need to invert the sdev.
        out[:] = 1 / np.log(close).diff().std()
    

# Compute final rank and assign long and short baskets.
def before_trading_start(context, data):
    results = pipeline_output('factors')
    
    next_earnings  = results['next_earnings']
    prev_earnings  = results['prev_earnings']
    
    # You can avoid the earnings_proximity here in case you haven't set up pipeline filters to do so 
    #results = results[ ~((results['next_earnings'] <= context.avoid_earnings_days) | (results['prev_earnings'] <= context.avoid_earnings_days)) ]
    
    results = results.drop(['next_earnings', 'prev_earnings'], axis= 1)
    
    results = results.replace([np.inf, -np.inf], np.nan)
    results = results.dropna()
    
    print 'Basket of stocks %d' % (len(results))
    
    ranks = results.rank().prod(axis=1).order(ascending=True)
          
    context.shorts = ranks.head(context.num_short_securities).order(ascending=True)
    context.longs  = ranks.tail(context.num_long_securities).order(ascending=False)
    
    for i in range(context.num_short_securities):
        context.shorts.iloc[i] = float(context.num_short_securities - i)
        
    for i in range(context.num_long_securities):
        context.longs.iloc[i]  = float(context.num_long_securities - i)
   
    if context.rank_weighted:
        #
        # rank weighted
        #
        context.shorts /= context.shorts.sum()
        context.longs  /= context.longs.sum()
    else:
        # equal weight
        context.shorts[:] = 1.0/len(context.shorts.index)
        context.longs[:]  = 1.0/len(context.longs.index)
    
    context.shorts *= context.leverage / 2.0
    context.longs  *= context.leverage / 2.0
    
    print 'shorts weighted (length %d, sum %f):\n' % (len(context.shorts.index), context.shorts.sum()), context.shorts.head(3), context.shorts.tail(3)
    print 'longs  weighted (length %d, sum %f):\n' % (len(context.longs.index), context.longs.sum()), context.longs.head(3), context.longs.tail(3)
    
    update_universe(context.longs.index | context.shorts.index)
    
    stocks = context.longs.index | context.shorts.index
    context.next_earnings  = next_earnings.ix[stocks]
    context.prev_earnings  = prev_earnings.ix[stocks]
    
# Put any initialization logic here. The context object will be passed to
# the other methods in your algorithm.
def initialize(context):
    
    context.leverage = 2.0
      
    context.num_long_securities  = 200
    context.num_short_securities = 200
    context.rank_weighted  = True # equal weighted or rank weighted 
    
    context.avoid_earnings_days = 3    
    
    pipe = Pipeline()
    pipe = attach_pipeline(pipe, name='factors')
    
    pipe.add(Value(), "value")
    pipe.add(Momentum(), "momentum")
    pipe.add(Quality(), "quality")
    pipe.add(Volatility(), "volatility")
    """
    Risk Framework
    There are two options: Screen your securities out in initialize() or
    do it right before you order. Both have slightly different behaviors.
    If you choose to do it in initialize, the initial cohort of stocks that
    you filter through in `before_trading_start` will not have an earnings
    announcement soon. If you choose to do it before you order, you're keeping
    all securities that might've met your initial SMA & dollar volume filter
    and choosing to take them out afterwards. Both work, but it will depend 
    on your strategy
    """
    # Option 1 of the risk framework. We choose to filter out
    # securities in initialize.
    # EarningsCalendar.X is the actual date of the announcement
    # E.g. 9/12/2015
    # pipe.add(EarningsCalendar.next_announcement.latest, 'next')
    # pipe.add(EarningsCalendar.previous_announcement.latest, 'prev')
    # BusinessDaysX is the integer days until or after the closest
    # announcement. So if AAPL had an earnings announcement yesterday,
    # prev_earnings would be 1. If it's the day of, it will be 0.
    # For BusinessDaysUntilNextEarnings(), it is common that the value
    # is NaaN because we typically don't know the precise date of an
    # earnings announcement until about 15 days before
    ne = BusinessDaysUntilNextEarnings()
    pe = BusinessDaysSincePreviousEarnings()
    pipe.add(ne, 'next_earnings')
    pipe.add(pe, 'prev_earnings')
    # The number of days before/after an announcement that you want to
    # avoid an earnings for.
    dollar_volume = AvgDailyDollarVolumeTraded()
    sma_200 = SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=200)
    pipe.set_screen((ne.isnan() | (ne > context.avoid_earnings_days) | (pe > context.avoid_earnings_days)) & (sma_200 > 5) & (dollar_volume > 10**7))
    
    
    # Option 2 of the risk framework. We choose to filter out
    # securities with earnings calendars RIGHT before we order instead of
    # in initialize, so the original logic of the algorithm isn't changed.
    #pipe.add(BusinessDaysUntilNextEarnings(), 'next_earnings')
    #pipe.add(BusinessDaysSincePreviousEarnings(), 'prev_earnings')
    # Screen out penny stocks and low liquidity securities.
    #sma_200 = SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=200)
    #dollar_volume = AvgDailyDollarVolumeTraded()    
    #pipe.set_screen((sma_200 > 5) & (dollar_volume > 10**7))    
    """
    Risk Framework End
    """
    
    context.spy = sid(8554)
    context.shorts = None
    context.longs = None
    
    schedule_function(rebalance, date_rules.month_start())
    schedule_function(cancel_open_orders, date_rules.every_day(),
                      time_rules.market_close())

    
# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    record(lever=context.account.leverage,
           exposure=context.account.net_leverage,
           num_pos=len(context.portfolio.positions),
           oo=len(get_open_orders()))

    
def cancel_open_orders(context, data):
    for security in get_open_orders():
        for order in get_open_orders(security):
            cancel_order(order)
        
def check_earnings_proximity(context, security):
    # For Option 2: Checks whether or not your security currently has
    # an earnings announcement within X days. Where X is defined by
    # context.avoid_earnings_days
    next_earnings = context.next_earnings.ix[security]
    prev_earnings = context.prev_earnings.ix[security]
    if next_earnings <= context.avoid_earnings_days or prev_earnings <= context.avoid_earnings_days:
        return True
    return False

def rebalance(context, data):
    for security in context.shorts.index:
        if get_open_orders(security):
            continue
        if security in data:
            if check_earnings_proximity(context, security):
                log.info("Earnings calendar release too close, not shorting %s!" % security.symbol)
            else:
                order_target_percent(security, -context.shorts[security])
            
    for security in context.longs.index:
        if get_open_orders(security):
            continue
        if security in data:
            if check_earnings_proximity(context, security):
                log.info("Earnings calendar release too close, not longing %s!" % security.symbol)
            else:
                order_target_percent(security, context.longs[security])
            
    for security in context.portfolio.positions:
        if get_open_orders(security):
            continue
        if security in data:
            if security not in (context.longs.index + context.shorts.index):
                order_target_percent(security, 0)    
        
There was a runtime error.

Hey guys,

Thanks for all your comments. Luca, the work you did was awesome! I worked your framework in on a bimonthly basis using a 15 day before/after window.

I found small improvements in returns, volatility, and drawdown compared to the version without the framework - that being said, the overall performance of both algorithms weren't great.

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
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import CustomFactor, SimpleMovingAverage
from quantopian.pipeline.data import morningstar

# For full information on the dataset please visit Quantopian data at
# https://www.quantopian.com/data/eventvestor/earnings_calendar
# Both Free & Paid versions will be accessed through the same
# namespace
from quantopian.pipeline.data.eventvestor import EarningsCalendar
from quantopian.pipeline.data.eventvestor.factors import BusinessDaysUntilNextEarnings, BusinessDaysSincePreviousEarnings

import pandas as pd
import numpy as np


class AvgDailyDollarVolumeTraded(CustomFactor):
    
    inputs = [USEquityPricing.close, USEquityPricing.volume]
    window_length = 20
    
    def compute(self, today, assets, out, close_price, volume):
        out[:] = np.mean(close_price * volume, axis=0)
        

class Value(CustomFactor):
    
    inputs = [morningstar.valuation_ratios.book_value_yield,
              morningstar.valuation_ratios.sales_yield,
              morningstar.valuation_ratios.fcf_yield] 
    
    window_length = 1
    
    def compute(self, today, assets, out, book_value, sales, fcf):
        value_table = pd.DataFrame(index=assets)
        value_table["book_value"] = book_value[-1]
        value_table["sales"] = sales[-1]
        value_table["fcf"] = fcf[-1]
        out[:] = value_table.rank().mean(axis=1)
    
    
class Momentum(CustomFactor):
    
    inputs = [USEquityPricing.close]
    window_length = 252
    
    def compute(self, today, assets, out, close):       
        out[:] = close[-20] / close[0]

            
class Quality(CustomFactor):
    
    inputs = [morningstar.operation_ratios.roe]
    window_length = 1
    
    def compute(self, today, assets, out, roe):       
        out[:] = roe[-1]
        
        
class Volatility(CustomFactor):
    
    inputs = [USEquityPricing.close]
    window_length = 252
    
    def compute(self, today, assets, out, close):  
        close = pd.DataFrame(data=close, columns=assets) 
        # Since we are going to rank largest is best we need to invert the sdev.
        out[:] = 1 / np.log(close).diff().std()
    

# Compute final rank and assign long and short baskets.
def before_trading_start(context, data):
    results = pipeline_output('factors')
    
    next_earnings  = results['next_earnings']
    prev_earnings  = results['prev_earnings']
    
    # You can avoid the earnings_proximity here in case you haven't set up pipeline filters to do so 
    #results = results[ ~((results['next_earnings'] <= context.avoid_earnings_days) | (results['prev_earnings'] <= context.avoid_earnings_days)) ]
    
    results = results.drop(['next_earnings', 'prev_earnings'], axis= 1)
    
    results = results.replace([np.inf, -np.inf], np.nan)
    results = results.dropna()
    
    print 'Basket of stocks %d' % (len(results))
    
    ranks = results.rank().prod(axis=1).order(ascending=True)
          
    context.shorts = ranks.head(context.num_short_securities).order(ascending=True)
    context.longs  = ranks.tail(context.num_long_securities).order(ascending=False)
    
    for i in range(context.num_short_securities):
        context.shorts.iloc[i] = float(context.num_short_securities - i)
        
    for i in range(context.num_long_securities):
        context.longs.iloc[i]  = float(context.num_long_securities - i)
   
    if context.rank_weighted:
        #
        # rank weighted
        #
        context.shorts /= context.shorts.sum()
        context.longs  /= context.longs.sum()
    else:
        # equal weight
        context.shorts[:] = 1.0/len(context.shorts.index)
        context.longs[:]  = 1.0/len(context.longs.index)
    
    context.shorts *= context.leverage / 2.0
    context.longs  *= context.leverage / 2.0
    
    print 'shorts weighted (length %d, sum %f):\n' % (len(context.shorts.index), context.shorts.sum()), context.shorts.head(3), context.shorts.tail(3)
    print 'longs  weighted (length %d, sum %f):\n' % (len(context.longs.index), context.longs.sum()), context.longs.head(3), context.longs.tail(3)
    
    update_universe(context.longs.index | context.shorts.index)
    
    stocks = context.longs.index | context.shorts.index
    context.next_earnings  = next_earnings.ix[stocks]
    context.prev_earnings  = prev_earnings.ix[stocks]
    
# Put any initialization logic here. The context object will be passed to
# the other methods in your algorithm.
def initialize(context):
    
    context.leverage = 2.0
      
    context.num_long_securities  = 200
    context.num_short_securities = 200
    context.rank_weighted  = True # equal weighted or rank weighted 
    
    context.avoid_earnings_days = 15    
    
    pipe = Pipeline()
    pipe = attach_pipeline(pipe, name='factors')
    
    pipe.add(Value(), "value")
    pipe.add(Momentum(), "momentum")
    pipe.add(Quality(), "quality")
    pipe.add(Volatility(), "volatility")
    """
    Risk Framework
    There are two options: Screen your securities out in initialize() or
    do it right before you order. Both have slightly different behaviors.
    If you choose to do it in initialize, the initial cohort of stocks that
    you filter through in `before_trading_start` will not have an earnings
    announcement soon. If you choose to do it before you order, you're keeping
    all securities that might've met your initial SMA & dollar volume filter
    and choosing to take them out afterwards. Both work, but it will depend 
    on your strategy
    """
    # Option 1 of the risk framework. We choose to filter out
    # securities in initialize.
    # EarningsCalendar.X is the actual date of the announcement
    # E.g. 9/12/2015
    # pipe.add(EarningsCalendar.next_announcement.latest, 'next')
    # pipe.add(EarningsCalendar.previous_announcement.latest, 'prev')
    # BusinessDaysX is the integer days until or after the closest
    # announcement. So if AAPL had an earnings announcement yesterday,
    # prev_earnings would be 1. If it's the day of, it will be 0.
    # For BusinessDaysUntilNextEarnings(), it is common that the value
    # is NaaN because we typically don't know the precise date of an
    # earnings announcement until about 15 days before
    ne = BusinessDaysUntilNextEarnings()
    pe = BusinessDaysSincePreviousEarnings()
    pipe.add(ne, 'next_earnings')
    pipe.add(pe, 'prev_earnings')
    # The number of days before/after an announcement that you want to
    # avoid an earnings for.
    dollar_volume = AvgDailyDollarVolumeTraded()
    sma_200 = SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=200)
    pipe.set_screen((ne.isnan() | (ne > context.avoid_earnings_days) | (pe > context.avoid_earnings_days)) & (sma_200 > 5) & (dollar_volume > 10**7))
    
    
    # Option 2 of the risk framework. We choose to filter out
    # securities with earnings calendars RIGHT before we order instead of
    # in initialize, so the original logic of the algorithm isn't changed.
    #pipe.add(BusinessDaysUntilNextEarnings(), 'next_earnings')
    #pipe.add(BusinessDaysSincePreviousEarnings(), 'prev_earnings')
    # Screen out penny stocks and low liquidity securities.
    #sma_200 = SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=200)
    #dollar_volume = AvgDailyDollarVolumeTraded()    
    #pipe.set_screen((sma_200 > 5) & (dollar_volume > 10**7))    
    """
    Risk Framework End
    """
    
    context.spy = sid(8554)
    context.shorts = None
    context.longs = None
    
    schedule_function(rebalance, date_rules.month_start(days_offset=15))
    schedule_function(rebalance, date_rules.month_start())
    schedule_function(cancel_open_orders, date_rules.every_day(),
                      time_rules.market_close())

    
# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    record(lever=context.account.leverage,
           exposure=context.account.net_leverage,
           num_pos=len(context.portfolio.positions),
           oo=len(get_open_orders()))

    
def cancel_open_orders(context, data):
    for security in get_open_orders():
        for order in get_open_orders(security):
            cancel_order(order)
        
def check_earnings_proximity(context, security):
    # For Option 2: Checks whether or not your security currently has
    # an earnings announcement within X days. Where X is defined by
    # context.avoid_earnings_days
    next_earnings = context.next_earnings.ix[security]
    prev_earnings = context.prev_earnings.ix[security]
    if next_earnings <= context.avoid_earnings_days or prev_earnings <= context.avoid_earnings_days:
        return True
    return False

def rebalance(context, data):
    for security in context.shorts.index:
        if get_open_orders(security):
            continue
        if security in data:
            if check_earnings_proximity(context, security):
                log.info("Earnings calendar release too close, not shorting %s!" % security.symbol)
            else:
                order_target_percent(security, -context.shorts[security])
            
    for security in context.longs.index:
        if get_open_orders(security):
            continue
        if security in data:
            if check_earnings_proximity(context, security):
                log.info("Earnings calendar release too close, not longing %s!" % security.symbol)
            else:
                order_target_percent(security, context.longs[security])
            
    for security in context.portfolio.positions:
        if get_open_orders(security):
            continue
        if security in data:
            if security not in (context.longs.index + context.shorts.index):
                order_target_percent(security, 0)    
        
There was a runtime error.

Never mind - I see this is the Option 1 - why do the positions still not total 400 all the time?

EDIT: what I am getting at is that I suspect the differences in statistics is actually caused by the confounding factor/bug of the exposure/leverage oscillating quarterly, rather specifically anything to do with earnings. The genuine effect might be more clear with daily rebalancing.

Good point! I think the appropriate comparison would be to take multiple algorithms made for daily rebalancing and compare the results, similar to how I did with monthly rebalanced algorithms.

@Simon, there is a bug in the pipeline screening. The current filter leaves some securities that are going to be skipped right before ordering them (check_earnings_proximity). Here is a fixed version, that is identical to Seong Lee's latest version with only the pipeline screening fixed.

Please note:
- that there is still a drop in the number of positions hold sometimes , but this is due to the limited number of securities in the pipeline basket.
- The net exposure has improved too.
- The overall performance are terrible

Clone Algorithm
89
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.factors import CustomFactor, SimpleMovingAverage
from quantopian.pipeline.data import morningstar

# For full information on the dataset please visit Quantopian data at
# https://www.quantopian.com/data/eventvestor/earnings_calendar
# Both Free & Paid versions will be accessed through the same
# namespace
from quantopian.pipeline.data.eventvestor import EarningsCalendar
from quantopian.pipeline.data.eventvestor.factors import BusinessDaysUntilNextEarnings, BusinessDaysSincePreviousEarnings

import pandas as pd
import numpy as np


class AvgDailyDollarVolumeTraded(CustomFactor):
    
    inputs = [USEquityPricing.close, USEquityPricing.volume]
    window_length = 20
    
    def compute(self, today, assets, out, close_price, volume):
        out[:] = np.mean(close_price * volume, axis=0)
        

class Value(CustomFactor):
    
    inputs = [morningstar.valuation_ratios.book_value_yield,
              morningstar.valuation_ratios.sales_yield,
              morningstar.valuation_ratios.fcf_yield] 
    
    window_length = 1
    
    def compute(self, today, assets, out, book_value, sales, fcf):
        value_table = pd.DataFrame(index=assets)
        value_table["book_value"] = book_value[-1]
        value_table["sales"] = sales[-1]
        value_table["fcf"] = fcf[-1]
        out[:] = value_table.rank().mean(axis=1)
    
    
class Momentum(CustomFactor):
    
    inputs = [USEquityPricing.close]
    window_length = 252
    
    def compute(self, today, assets, out, close):       
        out[:] = close[-20] / close[0]

            
class Quality(CustomFactor):
    
    inputs = [morningstar.operation_ratios.roe]
    window_length = 1
    
    def compute(self, today, assets, out, roe):       
        out[:] = roe[-1]
        
        
class Volatility(CustomFactor):
    
    inputs = [USEquityPricing.close]
    window_length = 252
    
    def compute(self, today, assets, out, close):  
        close = pd.DataFrame(data=close, columns=assets) 
        # Since we are going to rank largest is best we need to invert the sdev.
        out[:] = 1 / np.log(close).diff().std()
    

# Compute final rank and assign long and short baskets.
def before_trading_start(context, data):
    results = pipeline_output('factors')
    
    next_earnings  = results['next_earnings']
    prev_earnings  = results['prev_earnings']
    
    # You can avoid the earnings_proximity here in case you haven't set up pipeline filters to do so 
    #results = results[ ~((results['next_earnings'] <= context.avoid_earnings_days) | (results['prev_earnings'] <= context.avoid_earnings_days)) ]
    
    results = results.drop(['next_earnings', 'prev_earnings'], axis= 1)
    
    results = results.replace([np.inf, -np.inf], np.nan)
    results = results.dropna()
    
    print 'Basket of stocks %d' % (len(results))
    
    ranks = results.rank().prod(axis=1).order(ascending=True)
          
    context.shorts = ranks.head(context.num_short_securities).order(ascending=True)
    context.longs  = ranks.tail(context.num_long_securities).order(ascending=False)
    
    for i in range(context.num_short_securities):
        context.shorts.iloc[i] = float(context.num_short_securities - i)
        
    for i in range(context.num_long_securities):
        context.longs.iloc[i]  = float(context.num_long_securities - i)
   
    if context.rank_weighted:
        #
        # rank weighted
        #
        context.shorts /= context.shorts.sum()
        context.longs  /= context.longs.sum()
    else:
        # equal weight
        context.shorts[:] = 1.0/len(context.shorts.index)
        context.longs[:]  = 1.0/len(context.longs.index)
    
    context.shorts *= context.leverage / 2.0
    context.longs  *= context.leverage / 2.0
    
    print 'shorts weighted (length %d, sum %f):\n' % (len(context.shorts.index), context.shorts.sum()), context.shorts.head(3), context.shorts.tail(3)
    print 'longs  weighted (length %d, sum %f):\n' % (len(context.longs.index), context.longs.sum()), context.longs.head(3), context.longs.tail(3)
    
    update_universe(context.longs.index | context.shorts.index)
    
    stocks = context.longs.index | context.shorts.index
    context.next_earnings  = next_earnings.ix[stocks]
    context.prev_earnings  = prev_earnings.ix[stocks]
    
# Put any initialization logic here. The context object will be passed to
# the other methods in your algorithm.
def initialize(context):
    
    context.leverage = 2.0
      
    context.num_long_securities  = 200
    context.num_short_securities = 200
    context.rank_weighted  = True # equal weighted or rank weighted 
    
    context.avoid_earnings_days = 15    
    
    pipe = Pipeline()
    pipe = attach_pipeline(pipe, name='factors')
    
    pipe.add(Value(), "value")
    pipe.add(Momentum(), "momentum")
    pipe.add(Quality(), "quality")
    pipe.add(Volatility(), "volatility")
    """
    Risk Framework
    There are two options: Screen your securities out in initialize() or
    do it right before you order. Both have slightly different behaviors.
    If you choose to do it in initialize, the initial cohort of stocks that
    you filter through in `before_trading_start` will not have an earnings
    announcement soon. If you choose to do it before you order, you're keeping
    all securities that might've met your initial SMA & dollar volume filter
    and choosing to take them out afterwards. Both work, but it will depend 
    on your strategy
    """
    # Option 1 of the risk framework. We choose to filter out
    # securities in initialize.
    # EarningsCalendar.X is the actual date of the announcement
    # E.g. 9/12/2015
    # pipe.add(EarningsCalendar.next_announcement.latest, 'next')
    # pipe.add(EarningsCalendar.previous_announcement.latest, 'prev')
    # BusinessDaysX is the integer days until or after the closest
    # announcement. So if AAPL had an earnings announcement yesterday,
    # prev_earnings would be 1. If it's the day of, it will be 0.
    # For BusinessDaysUntilNextEarnings(), it is common that the value
    # is NaaN because we typically don't know the precise date of an
    # earnings announcement until about 15 days before
    ne = BusinessDaysUntilNextEarnings()
    pe = BusinessDaysSincePreviousEarnings()
    pipe.add(ne, 'next_earnings')
    pipe.add(pe, 'prev_earnings')
    # The number of days before/after an announcement that you want to
    # avoid an earnings for.
    dollar_volume = AvgDailyDollarVolumeTraded()
    sma_200 = SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=200)
    filters = ~((ne <= context.avoid_earnings_days) | (pe <= context.avoid_earnings_days)) \
              & (sma_200 > 5) & (dollar_volume > 10**7)
    pipe.set_screen(filters)
    
    
    # Option 2 of the risk framework. We choose to filter out
    # securities with earnings calendars RIGHT before we order instead of
    # in initialize, so the original logic of the algorithm isn't changed.
    #pipe.add(BusinessDaysUntilNextEarnings(), 'next_earnings')
    #pipe.add(BusinessDaysSincePreviousEarnings(), 'prev_earnings')
    # Screen out penny stocks and low liquidity securities.
    #sma_200 = SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=200)
    #dollar_volume = AvgDailyDollarVolumeTraded()    
    #pipe.set_screen((sma_200 > 5) & (dollar_volume > 10**7))    
    """
    Risk Framework End
    """
    
    context.spy = sid(8554)
    context.shorts = None
    context.longs = None
    
    schedule_function(rebalance, date_rules.month_start(days_offset=15))
    schedule_function(rebalance, date_rules.month_start())
    schedule_function(cancel_open_orders, date_rules.every_day(),
                      time_rules.market_close())

    
# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    record(lever=context.account.leverage,
           exposure=context.account.net_leverage,
           num_pos=len(context.portfolio.positions),
           oo=len(get_open_orders()))

    
def cancel_open_orders(context, data):
    for security in get_open_orders():
        for order in get_open_orders(security):
            cancel_order(order)
        
def check_earnings_proximity(context, security):
    # For Option 2: Checks whether or not your security currently has
    # an earnings announcement within X days. Where X is defined by
    # context.avoid_earnings_days
    next_earnings = context.next_earnings.ix[security]
    prev_earnings = context.prev_earnings.ix[security]
    if next_earnings <= context.avoid_earnings_days or prev_earnings <= context.avoid_earnings_days:
        return True
    return False

def rebalance(context, data):
    for security in context.shorts.index:
        if get_open_orders(security):
            continue
        if security in data:
            if check_earnings_proximity(context, security):
                log.info("Earnings calendar release too close, not shorting %s!" % security.symbol)
            else:
                order_target_percent(security, -context.shorts[security])
            
    for security in context.longs.index:
        if get_open_orders(security):
            continue
        if security in data:
            if check_earnings_proximity(context, security):
                log.info("Earnings calendar release too close, not longing %s!" % security.symbol)
            else:
                order_target_percent(security, context.longs[security])
            
    for security in context.portfolio.positions:
        if get_open_orders(security):
            continue
        if security in data:
            if security not in (context.longs.index + context.shorts.index):
                order_target_percent(security, 0)    
        
There was a runtime error.

So, this is the original algorithm (monthly rebalancing, avoid_earnings_days = 3) with the proper screening in the pipeline.

Bottom line: I cannot see any improvements when accounting for earnings announcements

Edit: well, to be sure that there aren't any improvements I should actually run this exact code disabling earnings announcement logic...but I am too annoyed by the far too many backtest timeouts. I leave this to the community

Clone Algorithm
89
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.factors import CustomFactor, SimpleMovingAverage
from quantopian.pipeline.data import morningstar

# For full information on the dataset please visit Quantopian data at
# https://www.quantopian.com/data/eventvestor/earnings_calendar
# Both Free & Paid versions will be accessed through the same
# namespace
from quantopian.pipeline.data.eventvestor import EarningsCalendar
from quantopian.pipeline.data.eventvestor.factors import BusinessDaysUntilNextEarnings, BusinessDaysSincePreviousEarnings

import pandas as pd
import numpy as np


class AvgDailyDollarVolumeTraded(CustomFactor):
    
    inputs = [USEquityPricing.close, USEquityPricing.volume]
    window_length = 20
    
    def compute(self, today, assets, out, close_price, volume):
        out[:] = np.mean(close_price * volume, axis=0)
        

class Value(CustomFactor):
    
    inputs = [morningstar.valuation_ratios.book_value_yield,
              morningstar.valuation_ratios.sales_yield,
              morningstar.valuation_ratios.fcf_yield] 
    
    window_length = 1
    
    def compute(self, today, assets, out, book_value, sales, fcf):
        value_table = pd.DataFrame(index=assets)
        value_table["book_value"] = book_value[-1]
        value_table["sales"] = sales[-1]
        value_table["fcf"] = fcf[-1]
        out[:] = value_table.rank().mean(axis=1)
    
    
class Momentum(CustomFactor):
    
    inputs = [USEquityPricing.close]
    window_length = 252
    
    def compute(self, today, assets, out, close):       
        out[:] = close[-20] / close[0]

            
class Quality(CustomFactor):
    
    inputs = [morningstar.operation_ratios.roe]
    window_length = 1
    
    def compute(self, today, assets, out, roe):       
        out[:] = roe[-1]
        
        
class Volatility(CustomFactor):
    
    inputs = [USEquityPricing.close]
    window_length = 252
    
    def compute(self, today, assets, out, close):  
        close = pd.DataFrame(data=close, columns=assets) 
        # Since we are going to rank largest is best we need to invert the sdev.
        out[:] = 1 / np.log(close).diff().std()
    

# Compute final rank and assign long and short baskets.
def before_trading_start(context, data):
    results = pipeline_output('factors')
    
    next_earnings  = results['next_earnings']
    prev_earnings  = results['prev_earnings']
    
    # You can avoid the earnings_proximity here in case you haven't set up pipeline filters to do so 
    #results = results[ ~((results['next_earnings'] <= context.avoid_earnings_days) | (results['prev_earnings'] <= context.avoid_earnings_days)) ]
    
    results = results.drop(['next_earnings', 'prev_earnings'], axis= 1)
    
    results = results.replace([np.inf, -np.inf], np.nan)
    results = results.dropna()
    
    print 'Basket of stocks %d' % (len(results))
    
    ranks = results.rank().prod(axis=1).order(ascending=True)
          
    context.shorts = ranks.head(context.num_short_securities).order(ascending=True)
    context.longs  = ranks.tail(context.num_long_securities).order(ascending=False)
    
    for i in range(context.num_short_securities):
        context.shorts.iloc[i] = float(context.num_short_securities - i)
        
    for i in range(context.num_long_securities):
        context.longs.iloc[i]  = float(context.num_long_securities - i)
   
    if context.rank_weighted:
        #
        # rank weighted
        #
        context.shorts /= context.shorts.sum()
        context.longs  /= context.longs.sum()
    else:
        # equal weight
        context.shorts[:] = 1.0/len(context.shorts.index)
        context.longs[:]  = 1.0/len(context.longs.index)
    
    context.shorts *= context.leverage / 2.0
    context.longs  *= context.leverage / 2.0
    
    print 'shorts weighted (length %d, sum %f):\n' % (len(context.shorts.index), context.shorts.sum()), context.shorts.head(3), context.shorts.tail(3)
    print 'longs  weighted (length %d, sum %f):\n' % (len(context.longs.index), context.longs.sum()), context.longs.head(3), context.longs.tail(3)
    
    update_universe(context.longs.index | context.shorts.index)
    
    stocks = context.longs.index | context.shorts.index
    context.next_earnings  = next_earnings.ix[stocks]
    context.prev_earnings  = prev_earnings.ix[stocks]
    
# Put any initialization logic here. The context object will be passed to
# the other methods in your algorithm.
def initialize(context):
    
    context.leverage = 2.0
      
    context.num_long_securities  = 200
    context.num_short_securities = 200
    context.rank_weighted  = True # equal weighted or rank weighted 
    
    context.avoid_earnings_days = 3    
    
    pipe = Pipeline()
    pipe = attach_pipeline(pipe, name='factors')
    
    pipe.add(Value(), "value")
    pipe.add(Momentum(), "momentum")
    pipe.add(Quality(), "quality")
    pipe.add(Volatility(), "volatility")
    """
    Risk Framework
    There are two options: Screen your securities out in initialize() or
    do it right before you order. Both have slightly different behaviors.
    If you choose to do it in initialize, the initial cohort of stocks that
    you filter through in `before_trading_start` will not have an earnings
    announcement soon. If you choose to do it before you order, you're keeping
    all securities that might've met your initial SMA & dollar volume filter
    and choosing to take them out afterwards. Both work, but it will depend 
    on your strategy
    """
    # Option 1 of the risk framework. We choose to filter out
    # securities in initialize.
    # EarningsCalendar.X is the actual date of the announcement
    # E.g. 9/12/2015
    # pipe.add(EarningsCalendar.next_announcement.latest, 'next')
    # pipe.add(EarningsCalendar.previous_announcement.latest, 'prev')
    # BusinessDaysX is the integer days until or after the closest
    # announcement. So if AAPL had an earnings announcement yesterday,
    # prev_earnings would be 1. If it's the day of, it will be 0.
    # For BusinessDaysUntilNextEarnings(), it is common that the value
    # is NaaN because we typically don't know the precise date of an
    # earnings announcement until about 15 days before
    ne = BusinessDaysUntilNextEarnings()
    pe = BusinessDaysSincePreviousEarnings()
    pipe.add(ne, 'next_earnings')
    pipe.add(pe, 'prev_earnings')
    # The number of days before/after an announcement that you want to
    # avoid an earnings for.
    dollar_volume = AvgDailyDollarVolumeTraded()
    sma_200 = SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=200)
    filters = ~((ne <= context.avoid_earnings_days) | (pe <= context.avoid_earnings_days)) \
              & (sma_200 > 5) & (dollar_volume > 10**7)
    pipe.set_screen(filters)
    
    
    # Option 2 of the risk framework. We choose to filter out
    # securities with earnings calendars RIGHT before we order instead of
    # in initialize, so the original logic of the algorithm isn't changed.
    #pipe.add(BusinessDaysUntilNextEarnings(), 'next_earnings')
    #pipe.add(BusinessDaysSincePreviousEarnings(), 'prev_earnings')
    # Screen out penny stocks and low liquidity securities.
    #sma_200 = SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=200)
    #dollar_volume = AvgDailyDollarVolumeTraded()    
    #pipe.set_screen((sma_200 > 5) & (dollar_volume > 10**7))    
    """
    Risk Framework End
    """
    
    context.spy = sid(8554)
    context.shorts = None
    context.longs = None
    
    #schedule_function(rebalance, date_rules.month_start(days_offset=15))
    schedule_function(rebalance, date_rules.month_start())
    schedule_function(cancel_open_orders, date_rules.every_day(),
                      time_rules.market_close())

    
# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    record(lever=context.account.leverage,
           exposure=context.account.net_leverage,
           num_pos=len(context.portfolio.positions),
           oo=len(get_open_orders()))

    
def cancel_open_orders(context, data):
    for security in get_open_orders():
        for order in get_open_orders(security):
            cancel_order(order)
        
def check_earnings_proximity(context, security):
    # For Option 2: Checks whether or not your security currently has
    # an earnings announcement within X days. Where X is defined by
    # context.avoid_earnings_days
    next_earnings = context.next_earnings.ix[security]
    prev_earnings = context.prev_earnings.ix[security]
    if next_earnings <= context.avoid_earnings_days or prev_earnings <= context.avoid_earnings_days:
        return True
    return False

def rebalance(context, data):
    for security in context.shorts.index:
        if get_open_orders(security):
            continue
        if security in data:
            if check_earnings_proximity(context, security):
                log.info("Earnings calendar release too close, not shorting %s!" % security.symbol)
            else:
                order_target_percent(security, -context.shorts[security])
            
    for security in context.longs.index:
        if get_open_orders(security):
            continue
        if security in data:
            if check_earnings_proximity(context, security):
                log.info("Earnings calendar release too close, not longing %s!" % security.symbol)
            else:
                order_target_percent(security, context.longs[security])
            
    for security in context.portfolio.positions:
        if get_open_orders(security):
            continue
        if security in data:
            if security not in (context.longs.index + context.shorts.index):
                order_target_percent(security, 0)    
        
There was a runtime error.

Great update!

When will DividendsCalendar be available in pipeline? (and perhaps some of the other EventVestor datasets)

Hi Luca,

Thanks for posting that, I think this original screen that I had used for option 1 and is probably the proper screen to use instead:

pipe.set_screen((ne.isnan() | (ne > context.avoid_earnings_days)) & (pe > context.avoid_earnings_days) & (sma_200 > 5) & (dollar_volume > 10**7))

James,

We're actively working towards that although I don't have a definite timeline for you. I'll let you know as soon as we do - but in the meanwhile, I'd love to get your thoughts on what kinds of factors you'd like to see.

In this case we have BusinessDaysUntilNextEarnings - would you be interested in seeing the same for dividends?

Thanks Seong,

Pretty similar to what you've got here -- Days Before Event, Days After Event.

@Seong: my bad, I erroneously changed the screen. But I fixed it again in the last two backtests I posted here and the risk metrics are not better than the original algorithm (the one that doesn't take into account the earning announcements).

The only algorithm that shows improvements in the risk metrics in the one that filters out stocks right before ordering, when weights have been already assigned. Because that creates an unbalanced (long/short exposure) portfolio we cannot say for sure the improvements are due to the filtering or due to the unbalanced portfolio.

Hi Luca,

Thanks for your comments, I appreciate them and think you have some pretty good points.

Here's another example using the risk framework taken from the Quantopian Lectures Long-Short Equity Strategy but modified for a weekly rebalance period.

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
"""
This algorithm demonstrates the concept of long-short equity. It uses two fundamental factors to rank all equities. It then longs the top of the ranking and shorts the bottom. For information on long-short equity strategies, please see the corresponding lecture on

https://www.quantopian.com/lectures

The dollar volume threshold is in place because orders of thinly traded securities can fail to fill in time and result in worse pricing and returns.

WARNING: These factors were selected because they worked in the past over the specific time period we choose. We do not anticipate them working in the future. In practice finding your own factors is the hardest part of developing any long-short equity strategy. This algorithm is meant to serve as a framework for testing your own ranking factors.

This algorithm was developed as part of 
Quantopian's Lecture Series. Please direct any 
questions, feedback, or corrections to [email protected]
"""
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.factors import CustomFactor, SimpleMovingAverage
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import morningstar

from quantopian.pipeline.data.eventvestor.factors import BusinessDaysUntilNextEarnings, BusinessDaysSincePreviousEarnings

import numpy as np
import pandas as pd


class Value(CustomFactor):
    inputs = [morningstar.income_statement.ebit,
              morningstar.valuation.enterprise_value]
    window_length = 1
    
    def compute(self, today, assets, out, ebit, ev):
        out[:] = ebit[-1] / ev[-1]
        
        
class Quality(CustomFactor):
    
    # Pre-declare inputs and window_length
    inputs = [morningstar.operation_ratios.roe,]
    window_length = 1
    
    def compute(self, today, assets, out, roe):
        out[:] = roe[-1]
        
        
class AvgDailyDollarVolumeTraded(CustomFactor):
    inputs = [USEquityPricing.close, USEquityPricing.volume]
    
    def compute(self, today, assets, out, close_price, volume):
        out[:] = np.mean(close_price * volume, axis=0)

        
def make_pipeline():
    """
    Create and return our pipeline.
    
    We break this piece of logic out into its own function to make it easier to
    test and modify in isolation.
    
    In particular, this function can be copy/pasted into research and run by itself.
    """
    pipe = Pipeline()

    # Basic value and quality metrics.
    value = Value()
    pipe.add(value, "value")
    quality = Quality()
    pipe.add(quality, "quality")
    
     # We only want to trade relatively liquid stocks.
    # Build a filter that only passes stocks that have $10,000,000 average
    # daily dollar volume over the last 20 days.
    dollar_volume = AvgDailyDollarVolumeTraded(window_length=20)
    is_liquid = (dollar_volume > 1e7)
    
    # We also don't want to trade penny stocks, which we define as any stock with an
    # average price of less than $5.00 over the last 200 days.
    sma_200 = SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=200)
    not_a_penny_stock = (sma_200 > 5)
    
    """
    Risk Framework
    """
    # Option 1 of the risk framework. We choose to filter out
    # securities in initialize.
    # EarningsCalendar.X is the actual date of the announcement
    # E.g. 9/12/2015
    # pipe.add(EarningsCalendar.next_announcement.latest, 'next')
    # pipe.add(EarningsCalendar.previous_announcement.latest, 'prev')
    # BusinessDaysX is the integer days until or after the closest
    # announcement. So if AAPL had an earnings announcement yesterday,
    # prev_earnings would be 1. If it's the day of, it will be 0.
    # For BusinessDaysUntilNextEarnings(), it is common that the value
    # is NaaN because we typically don't know the precise date of an
    # earnings announcement until about 15 days before
    ne = BusinessDaysUntilNextEarnings()
    pe = BusinessDaysSincePreviousEarnings()
    pipe.add(ne, 'next_earnings')
    pipe.add(pe, 'prev_earnings')
    # The number of days before/after an announcement that you want to
    # avoid an earnings for.
    avoid_earnings_days = 7
    does_not_have_earnings = ((ne.isnan() | (ne > avoid_earnings_days)) & (pe > avoid_earnings_days))
    """
    End of Risk Framework
    """
    
    # Before we do any other ranking, we want to throw away these assets.
    initial_screen = (is_liquid & not_a_penny_stock & does_not_have_earnings)

    # Construct and add a Factor representing the average rank of each asset by our 
    # value and quality metrics. 
    # By applying a mask to the rank computations, we remove any stocks that failed 
    # to meet our initial criteria **before** computing ranks.  This means that the 
    # stock with rank 10.0 is the 10th-lowest stock that passed `initial_screen`.
    combined_rank = (
        value.rank(mask=initial_screen) + 
        quality.rank(mask=initial_screen)
    )
    pipe.add(combined_rank, 'combined_rank')

    # Build Filters representing the top and bottom 200 stocks by our combined ranking system.
    # We'll use these as our tradeable universe each day.
    longs = combined_rank.top(200)
    shorts = combined_rank.bottom(200)

    # The final output of our pipeline should only include 
    # the top/bottom 200 stocks by our criteria.
    pipe.set_screen(longs | shorts)
    
    pipe.add(longs, 'longs')
    pipe.add(shorts, 'shorts')
    
    return pipe


def initialize(context):
    
    # Set slippage and commission to zero to evaulate the signal generating 
    # ability of the algorithm 
    set_commission(commission.PerShare(cost=0.0075, min_trade_cost=1.0))
    set_slippage(slippage.VolumeShareSlippage(volume_limit=0.025, price_impact=0.1))

    context.long_leverage = 0.50
    context.short_leverage = -0.50
    context.spy = sid(8554)
    
    attach_pipeline(make_pipeline(), 'ranking_example')
    
    # Used to avoid purchasing any leveraged ETFs 
    context.dont_buys = security_lists.leveraged_etf_list
     
    # Schedule my rebalance function weekly
    schedule_function(func=rebalance, 
                      date_rule=date_rules.week_start(days_offset=0), 
                      time_rule=time_rules.market_open(hours=0,minutes=30), 
                      half_days=True)
    

def before_trading_start(context, data):
    # Call pipeline_output to get the output
    # Note this is a dataframe where the index is the SIDs for all 
    # securities to pass my screen and the columns are the factors which
    output = pipeline_output('ranking_example')
    ranks = output['combined_rank']
    
    long_ranks = ranks[output['longs']]
    short_ranks = ranks[output['shorts']]

    context.long_weights = (long_ranks / long_ranks.sum())
    log.info("Long Weights:")
    log.info(context.long_weights)
    
    context.short_weights = (short_ranks / short_ranks.sum())
    log.info("Short Weights:")
    log.info(context.short_weights)
    
    context.active_portfolio = context.long_weights.index.union(context.short_weights.index)
    update_universe(context.active_portfolio)


def handle_data(context, data):  
    
    # Record and plot the leverage, number of positions, and expsoure of our portfolio over time. 
    record(num_positions=len(context.portfolio.positions),
           exposure=context.account.net_leverage, 
           leverage=context.account.leverage)
    

# This function is scheduled to run at the start of each month.
def rebalance(context, data):
    """
    Allocate our long/short portfolio based on the weights supplied by
    context.long_weights and context.short_weights.
    """
    # Order our longs.
    log.info("ordering longs")
    for long_stock, long_weight in context.long_weights.iterkv():
        if long_stock in data:
            if get_open_orders(sid=long_stock):
                continue
            if long_stock in context.dont_buys:
                continue
            order_target_percent(long_stock, context.long_leverage * long_weight)
    
    # Order our shorts.
    log.info("ordering shorts")
    for short_stock, short_weight in context.short_weights.iterkv():
        if short_stock in data:
            if get_open_orders(sid=short_stock):
                continue
            if short_stock in context.dont_buys:
                continue
            order_target_percent(short_stock, context.short_leverage * short_weight)
    
    # Sell any positions in assets that are no longer in our target portfolio.
    for security in context.portfolio.positions:
        if get_open_orders(security):
            continue
        if security in data:  # Work around inability to sell de-listed stocks.
            if security not in context.active_portfolio:
                order_target_percent(security, 0)
There was a runtime error.

Thanks for everyone who joined during the webinar on 2/2. If you missed it, we recorded it here.

I've also tried creating a much simpler algorithm that rebalances a few large market cap tech stocks while avoiding earnings.

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
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.factors import CustomFactor, SimpleMovingAverage
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import morningstar

from quantopian.pipeline.data.eventvestor.factors import BusinessDaysUntilNextEarnings, BusinessDaysSincePreviousEarnings

def make_pipeline():
    """
    Create and return our pipeline.
    
    We break this piece of logic out into its own function to make it easier to
    test and modify in isolation.
    
    In particular, this function can be copy/pasted into research and run by itself.
    """
    pipe = Pipeline()
    """
    Risk Framework
    """
    # EarningsCalendar.X is the actual date of the announcement
    # E.g. 9/12/2015
    # pipe.add(EarningsCalendar.next_announcement.latest, 'next')
    # pipe.add(EarningsCalendar.previous_announcement.latest, 'prev')
    # BusinessDaysX is the integer days until or after the closest
    # announcement. So if AAPL had an earnings announcement yesterday,
    # prev_earnings would be 1. If it's the day of, it will be 0.
    # For BusinessDaysUntilNextEarnings(), it is common that the value
    # is NaaN because we typically don't know the precise date of an
    # earnings announcement until about 15 days before
    ne = BusinessDaysUntilNextEarnings()
    pe = BusinessDaysSincePreviousEarnings()
    pipe.add(ne, 'next_earnings')
    pipe.add(pe, 'prev_earnings')
    # The number of days before/after an announcement that you want to
    # avoid an earnings for.
    avoid_earnings_days = 1
    does_not_have_earnings = ((ne.isnan() | (ne > avoid_earnings_days)) & (pe > avoid_earnings_days))
    # Comment in/out to toggle risk framework
    pipe.set_screen(does_not_have_earnings)
    """
    End of Risk Framework
    """
    return pipe

def before_trading_start(context, data):
    context.results = pipeline_output('tech_stocks')
    context.stocks_to_trade = [stock for stock in context.tech_stocks
                               if stock in context.results.index]
    

# Put any initialization logic here.  The context object will be passed to
# the other methods in your algorithm.
def initialize(context):
    set_symbol_lookup_date('2015-01-01')
    context.spy = sid(8554)
    context.tech_stocks = [
        symbol('AAPL'),
        symbol('NFLX'),
        symbol('AMZN'),
        symbol('GOOG_L'),
        symbol('YHOO'),
        symbol('TSLA'),
        symbol('MSFT'),
        symbol('IBM'),]
    
    attach_pipeline(make_pipeline(), 'tech_stocks')
     
    # Schedule my rebalance function weekly
    schedule_function(func=rebalance, 
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open(hours=0,minutes=30), 
                      half_days=True)

def rebalance(context, data):
    for stock in context.stocks_to_trade:
        order_target_percent(stock, .5*1.0/len(context.stocks_to_trade))
    
    for stock in context.portfolio.positions:
        if stock not in context.stocks_to_trade and stock != context.spy:
            order_target_percent(stock, 0)
            log.info("Earnings Release too close for %s" % stock.symbol)
    
    # SPY Hedge only the amount that we're ordering
    order_target_percent(
        context.spy, -.5*len(context.stocks_to_trade)/len(context.tech_stocks))
    
# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    record(positions=len(context.stocks_to_trade),
           leverage=context.account.leverage)
There was a runtime error.

The second-to-last algorithm makes a pretty strong case for market efficiency!

Maybe I'm wrong, but I think, there is a timing inconsistency in the factors.
I mean the EBIT in the Value factor is the quarterly one, while the Net Income in the ROE of the Quality factor is TTM (trailing twelve months), isn't it?
Can you please confirm it?

There are TTM metrics exposed in our fundamentals data currently, to the best of my knowledge.

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.

Yes, the ratios (i.e. Roe, PS, etc...) are TTM but according to the documentation "Quantopian currently exposes the most recent period's data."... Or is it only for get_fundamentals true but not for the pipeline API?
Quantopian is a great platform... Anyway I hope that Quantopian will soon introduce a convenient way to handle historical fundamental data in the Pipeline API... I'm trying to implement the Piotroski score as pipeline factor, but without that is really difficult

Hi Constantino,

Have you seen this thread? (https://www.quantopian.com/posts/piotroski-long-slash-short-using-pipeline) Another member attempted to implement the piotroski score using pipeline. Thought you might find it useful.

Seong

@Josh: There are some metrics that uses TTM data, like for example some ratios, but the basic fundamentals data (for example Revenues, Costs, etc..) are quarterly based, making quite difficult so build some custom historical ratios. I wrote a small algorithm to check it.

@Seong: I'd seen that thread. The author Giuseppe Moriconi overcomes the time frame problem using TTM based ratios, but now always like in the case of the Operating Cash Flow: He uses the Cash Flow from the last quarter, while the original Piotroski score requires the data in the last 12 months. To keep it short, the limitation with the historical fundamental data remains, even if Giuseppe Moriconi found a smart workaround.

Just looking at using these for my mean reversion algo, and it seems to help. Am I right in understanding that zero days to next earnings means day of earnings report. This may be before market open or after market close (and very rarely during market hours). Since the pipeline is calculated at the end of the day, when I actually trade on the info, it's within a schedule function at market open the next day at the earliest?

Hi Dan,

I'll do my best to answer your question.

Am I right in understanding that zero days to next earnings means day of earnings report. This may be before market open or after market close (and very rarely during market hours).

This is correct.

Since the pipeline is calculated at the end of the day, when I actually trade on the info, it's within a schedule function at market open the next day at the earliest?

I'm not quite sure what you're asking, so I'll do my best to answer but let me know if this is not clear. Pipeline is calculated at the beginning of each day before trading starts. So when next == 0, for the current day you know that there was an earnings announcement either before market open or sometime later after market close.

When prev == 1, you know that yesterday there was an earnings announcement

Ah OK, that's what confused me. I thought pipeline was calculated at market close, i.e. the date of reckoning is the day before you can actually trade with the information it produces. But what you're saying is that it's calculated before market open, so you can then trade that same day.

My point was to understand what "next == 0" means, and I get that now, even though my brain aches like Marty McFly in Back To The Future 3.

An observation. The following two pieces of code produce the same results:

    has_earnings = ne.notnan() & (ne == 1)  
    pipe.set_screen(universe & ~has_earnings)  
    has_earnings = ne.notnan() & (ne == 0)  
    pipe.set_screen(universe & ~has_earnings)  

It took me a while to figure out, but I think it's because pipeline factors are floats, so exact equality comparison to integers isn't going to work. This is counter-intuitive, as days to earnings is always a whole number.

My work around is something horrible like:

    has_earnings = ne.notnan() & (ne > -1) & (ne < 1)  
    pipe.set_screen(universe & ~has_earnings)  

Any suggestions?

Ah yes, you're correct. I'm rereading Scott's post here https://www.quantopian.com/posts/pipeline-classifiers-are-here#57040f9fb58c6d60d00000c4 to refresh my memory.

It looks like this might work for you:

    has_earnings = (ne.notnan() & ne.eq(0))  
    pipe.set_screen(universe & ~has_earnings)  

Hello, is there any update regarding the issues related to the availability of these data in June 2015, thanks!

Thanks Francesco, we're working on a solution now and will update this thread once that's available.

Hello folks,

The data availability issue starting in June 2015 has since been resolved.

Let me know if you have any other questions,

Seong

Hi, I'm trying to add Earnings Announcements in the sample Pair-Trading Algorithm so that it doesn't hold or buy any asset since or close to it's earning announcements(eg. 7days). I have tried using pipeline and the method stated above but still getting some runtime error and couldn't figure it out. I'm not really sure what's wrong with my code or if I'm missing something?

Also, how can I use eventvestor data or import EarningsCalendar while using Zipline in my local environment?
Here' my code:

import numpy as np  
from quantopian.algorithm import attach_pipeline, pipeline_output  
from quantopian.pipeline import Pipeline  
# For use in your algorithms  
# Using the full dataset in your pipeline algo  
from quantopian.pipeline.data.eventvestor import EarningsCalendar

# To use built-in Pipeline factors for this dataset  
from quantopian.pipeline.factors.eventvestor import (  
BusinessDaysUntilNextEarnings,  
BusinessDaysSincePreviousEarnings  
)
import pandas as pd

def make_pipeline():  
    pipe = Pipeline()  
    ne = BusinessDaysUntilNextEarnings()  
    pe = BusinessDaysSincePreviousEarnings()  
    pipe.add(ne, 'next_earnings')  
    pipe.add(pe, 'prev_earnings')  
    # The number of days before/after an announcement that you want to  
    # avoid an earnings for.  
    avoid_earnings_days = 7  
    does_not_have_earnings = ((ne.isnan() | (ne > avoid_earnings_days)) & (pe > avoid_earnings_days))

    # Before we do any other ranking, we want to throw away these assets.  
    pipe.set_screen(does_not_have_earnings)  
    return pipe


def before_trading_start(context, data):  
    context.results = pipeline_output('factors')  


def check_earnings_proximity(context, security):  
    # For Option 2: Checks whether or not your security currently has  
    # an earnings announcement within X days. Where X is defined by  
    # context.avoid_earnings_days  
    next_earnings  = context.results['next_earnings']  
    prev_earnings  = context.results['prev_earnings']  
    if next_earnings <= context.avoid_earnings_days or prev_earnings <= context.avoid_earnings_days:  
        return True  
    return False

def initialize(context):  
    set_slippage(slippage.FixedSlippage(spread=0))  
    set_commission(commission.PerTrade(cost=.003))  
    attach_pipeline(make_pipeline(), 'factors')  
    #set_benchmark(sid(21774))  
    context.stock1 = symbol('CCE')  
    context.stock2 = symbol('KO')  
    context.security_list = [context.stock1, context.stock2]  
    context.threshold = 1  
    context.in_high = False  
    context.in_low = False  
    schedule_function(rebalance, date_rule=date_rules.every_day(), time_rule=time_rules.market_close(hours=1))

def rebalance(context, data):  
    canTrade = False  
    s1 = context.stock1  
    s2 = context.stock2  
    if len(get_open_orders()) > 0:  
        #return  
        for security in context.security_list:  
            if check_earnings_proximity(context, security):  
                canTrade = False  
                log.info("Earnings calendar release too close, not trading %s!" % security.symbol)  
            else:  
                canTrade = True  
        if not canTrade:  
            order_target_percent(s1, 0)  
            order_target_percent(s2, 0)  
            context.in_high = False  
            context.in_low = False  
    p60 = data.history(context.security_list, 'price', 60, '1d')  
    p5 = p60.iloc[-5:]  
    # Get the 60 day mavg  
    m60 = np.mean(p60[s1] - p60[s2])  
    # Get the std of the last 60 days  
    std60 = np.std(p60[s1] - p60[s2])  

    # Current diff = 5 day mavg  
    m5 = np.mean(p5[s1] - p5[s2])  
    # Compute z-score  
    if std60 > 0:  
        zscore = (m5 - m60)/std60  
    else:  
        zscore = 0  
    if zscore > context.threshold and not context.in_high and all(data.can_trade(context.security_list)):  
        for security in context.security_list:  
            if check_earnings_proximity(context, security):  
                canTrade = False  
                log.info("Earnings calendar release too close, not trading %s!" % security.symbol)  
            else:  
                canTrade = True  
        if canTrade:  
            order_target_percent(s1, -0.5) # short top  
            order_target_percent(s2, 0.5) # long bottom  
            context.in_high = True  
            context.in_low = False  
    elif zscore < -context.threshold and not context.in_low and all(data.can_trade(context.security_list)):  
        for security in context.security_list:  
            if check_earnings_proximity(context, security):  
                canTrade = False  
                log.info("Earnings calendar release too close, not trading %s!" % security.symbol)  
            else:  
                canTrade = True  
        if canTrade:  
            order_target_percent(s1, 0.5) # long top  
            order_target_percent(s2, -0.5) # short bottom  
            context.in_high = False  
            context.in_low = True  
    elif abs(zscore) < 1  and all(data.can_trade(context.security_list)):  
        order_target_percent(s1, 0)  
        order_target_percent(s2, 0)  
        context.in_high = False  
        context.in_low = False  
    record('zscore', zscore, lev=context.account.leverage)