Back to Community
Possible Issue with Estimize Data?

I've been playing around with Seong's Post-Earnings Drift Trading Strategy with Estimize (PEAD) algo and looked at incorporating the Wall Street EPS and Revenue numbers (reported by Estimize) as additional data points, but got strange results. I then started to compare the numbers that were being output in the log with what was on the Estimize site and discovered that the vast majority of the time the numbers were incorrect. The Estimize ESP, actual EPS, and even the Estimize revenue and actual revenue numbers all matched up perfectly, but the Wall Street EPS and Wall Stree revenue were wrong more often than not. A little more digging and it appears that the data shown is often from other quarters. Here is a sample log dated 1-24-2014 (I formatted it for easy reading and just made a screenshot of the table):

Screen Shot of Log Output (formatted)

Columns I and J are what's shown in the log and columns K and L are what the numbers should be. I then included notes about where I actually found numbers that correlated to the numbers reported in the log.

I've done a lot of testing and reviewing across many different dates and it's always the same; most of the data is incorrect (although not all). Thinking maybe it was a problem with the free data, I subscribed to the full Estimize data set and it's incorrect even up to the present. I've attached a short backtest of what I ran. I just took Seong's original algo, included the "consensus_wallstreet_eps_free" import and created two additional factors to show the Wall Street EPS and revenue. Everything else is the same.

Any idea what might be going on?

Scott

Clone Algorithm
4
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 is a PEAD strategy based off Estimize's earnings estimates. Estimize
is a service that aggregate financial estimates from independent, buy-side,
sell-side analysts as well as students and professors. You can run this
algorithm yourself by geting the free sample version of Estimize's consensus
dataset and EventVestor's Earnings Calendar Dataset at:

- https://www.quantopian.com/data/eventvestor/earnings_calendar
- https://www.quantopian.com/data/estimize/revisions

Much of the variables are meant for you to be able to play around with them:
1. context.days_to_hold: defines the number of days you want to hold before exiting a position
2. context.min/max_surprise: defines the min/max % surprise you want before trading on a signal
"""

import numpy as np

from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline import CustomFactor

# The sample version available from 18 Oct 2010 - 10 Feb 2015
from quantopian.pipeline.data.estimize import consensus_estimize_eps_free as estimize
# from quantopian.pipeline.data.estimize import consensus_estimize_revenue_free
from quantopian.pipeline.data.estimize import consensus_wallstreet_eps_free as ws
# from quantopian.pipeline.data.estimize import consensus_wallstreet_revenue_free

# The full version is available at https://www.quantopian.com/data/estimize/revisions
# from quantopian.pipeline.data.estimize import consensus_estimize_eps as estimize
# from quantopian.pipeline.data.estimize import consensus_estimize_revenue
# from quantopian.pipeline.data.estimize import consensus_wallstreet_eps
# from quantopian.pipeline.data.estimize import consensus_wallstreet_revenue

# The sample and full version is found through the same namespace
# https://www.quantopian.com/data/eventvestor/earnings_calendar
# Sample date ranges: 01 Jan 2007 - 10 Feb 2014
from quantopian.pipeline.data.eventvestor import EarningsCalendar
from quantopian.pipeline.factors.eventvestor import (
    BusinessDaysUntilNextEarnings,
    BusinessDaysSincePreviousEarnings
)

# Create custom factor subclass to calculate a market cap based on yesterday's
# close
class PercentSurprise(CustomFactor):
    window_length = 1
    inputs = [estimize.eps, estimize.estimize_eps_final]

    # Compute market cap value
    def compute(self, today, assets, out, actual_eps, estimize_eps):
        out[:] = (actual_eps[-1] - estimize_eps[-1])/(estimize_eps[-1] + 0)

def make_pipeline(context):
    # Create our pipeline
    pipe = Pipeline()

    # Get our Estimize Factors
    estimize_eps = estimize.estimize_eps_final
    actual_eps = estimize.eps
    wallstreet_eps = ws.wallstreet_eps_final
    wallstreet_rev = ws.wallstreet_revenue_final
    num_estimates = estimize.count
    percent_surprise = PercentSurprise()

    # Add Factors to Pipeline
    pipe.add(estimize_eps.latest, 'estimize_eps')
    pipe.add(actual_eps.latest, 'actual_eps')
    pipe.add(wallstreet_eps.latest, 'wallstreet_eps')
    pipe.add(wallstreet_rev.latest, 'wallstreet_rev')
    pipe.add(num_estimates.latest, 'num_estimates')
    pipe.add(percent_surprise, 'percent_surprise')
    pipe.add(BusinessDaysUntilNextEarnings(), 'ne')
    pipe.add(BusinessDaysSincePreviousEarnings(), 'pe')
    pipe.add(EarningsCalendar.next_announcement.latest, 'next')
 
    # Set our screen
    pipe.set_screen(
        actual_eps.latest.notnan() & (
        # Negative Surprise
        ((percent_surprise < -context.min_surprise) &
         (percent_surprise > -context.max_surprise))
        |
        # Positive Surprise
        ((percent_surprise >= context.min_surprise) &
         (percent_surprise <= context.max_surprise))
        ))

    return pipe
        
def initialize(context):
    #: Set commissions and slippage to 0 to determine pure alpha
    set_commission(commission.PerShare(cost=0, min_trade_cost=0))
    set_slippage(slippage.FixedSlippage(spread=0))

    #: Declaring the days to hold, change this to what you want)))
    context.days_to_hold = 3
    #: Declares which stocks we currently held and how many days we've held them dict[stock:days_held]
    context.stocks_held = {}
    #: Declares the minimum magnitude of percent surprise
    context.min_surprise = .00
    context.max_surprise = .04
   
    #: OPTIONAL - Initialize our Hedge
    # See order_positions for hedging logic
    context.spy = sid(8554)
    
    # Make our pipeline
    attach_pipeline(make_pipeline(context), 'estimize')

    
    # Log our positions at 10:00AM
    schedule_function(func=log_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())
    # Exit our positions
    schedule_function(func=exit_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_close(minutes=30))



def before_trading_start(context, data):
    # Screen for securities that only have an earnings release
    # 1 business day previous and separate out the earnings surprises into
    # positive and negative 
    results = pipeline_output('estimize')
    results = results[results['pe'] == 1]
    context.positive_surprise = results[results['percent_surprise'] > 0]
    context.negative_surprise = results[results['percent_surprise'] < 0]
    log.info(results.iloc[:5])
    log.info("There are %s positive surprises today and %s negative surprises" % 
             (len(context.positive_surprise.index), len(context.negative_surprise.index)))
    update_universe(context.positive_surprise.index | context.negative_surprise.index)

def log_positions(context, data):
    #: Get all positions
    all_positions = "Current positions for %s : " % (str(get_datetime()))
    for pos in context.portfolio.positions:
        if context.portfolio.positions[pos].amount != 0:
            all_positions += "%s at %s shares, " % (pos.symbol, context.portfolio.positions[pos].amount)
    log.info(all_positions)
        
def order_positions(context, data):
    """
    Main ordering conditions to always order an equal percentage in each position
    so it does a rolling rebalance by looking at the stocks to order today and the stocks
    we currently hold in our portfolio.
    """
    port = context.portfolio.positions
    current_positive_pos = [pos for pos in port if (port[pos].amount > 0 and pos in context.stocks_held)]
    current_negative_pos = [pos for pos in port if (port[pos].amount < 0 and pos in context.stocks_held)]
    negative_stocks = context.negative_surprise.index.tolist() + current_negative_pos
    positive_stocks = context.positive_surprise.index.tolist() + current_positive_pos
    
    # Rebalance our negative surprise securities (existing + new)
    for security in negative_stocks:
        if get_open_orders(security):
            continue
        if security in data:
            order_target_percent(security, -1.0 / len(negative_stocks))
            if context.stocks_held.get(security) is None:
                context.stocks_held[security] = 0

    # Rebalance our positive surprise securities (existing + new)                
    for security in positive_stocks:
        if get_open_orders(security):
            continue
        if security in data:
            order_target_percent(security, 1.0 / len(positive_stocks))
            if context.stocks_held.get(security) is None:
                context.stocks_held[security] = 0

    #: Get the total amount ordered for the day
    amount_ordered = 0 
    for order in get_open_orders():
        for oo in get_open_orders()[order]:
            amount_ordered += oo.amount * data[oo.sid].price

    #: Order our hedge
    # order_target_value(context.spy, -amount_ordered)
    # context.stocks_held[context.spy] = 0
    # log.info("We currently have a net order of $%0.2f and will hedge with SPY by ordering $%0.2f" % (amount_ordered, -amount_ordered))
        
def exit_positions(context, data):        
    """
    Exit position/days held update logic
    """
    #: Go through each held stock and update the number of days held and close out any positions
    #: that have been held past context.days_to_hold        
    for security in context.portfolio.positions:
        
        #: If we don't currently hold the stock or there are open orders, don't try and exit
        if context.stocks_held.get(security) is None:
            log.info("Cannot exit %s" % security.symbol)
            continue

        # Make sure that our days held are incremented
        context.stocks_held[security] += 1     

        # Exit our position and delete it from our list of securities currently held
        if context.stocks_held[security] >= context.days_to_hold:
            order_target_percent(security, 0)
            del context.stocks_held[security]
        
def handle_data(context, data):      
    record(leverage=context.account.leverage)
There was a runtime error.
5 responses

Has anyone had a chance to look into this issue?

Scott

Yikes!

We're digging in here. Will let you know what we find.

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.

any result?

Hi Peter,

We were able to diagnose the problem yesterday. We're working on a solution. It will likely require re-building the data set so it won't be a quick fix but it's top of the queue for us and we're continuing to hack away at it.

Thanks
Josh