Back to Community
FactSet Estimates Example Algorithm

The algorithm attached here is a great example of how FactSet Estimates can be used. It's a revamped version of the algorithm found here. There are differences between the two versions:
- Unlike the original, this version uses the QTradableStocksUS universe and also Optimize since they are required for the contest.
- This version uses FactSet Estimates instead of Zacks and EventVestor data.
- This version performs better by going short on positive surprises and going long on the misses, which is the opposite of the original.

Clone Algorithm
98
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 example demonstrates how to use earnings data in your algorithm. The strategy is to short stocks that beat earnings and have a positive news sentiment. Likewise it long stocks which missed earnings estimates and have a negative sentiment. The amount ordered of each is based upon the earnings surprise. It closes positions after a fixed time since the announcement. This is a contrarian strategy which assumes traders over-react to news.
'''

# Import necessary Pipeline modules
from quantopian.pipeline import Pipeline
from quantopian.algorithm import attach_pipeline, pipeline_output

# Import specific filters and factors which will be used
from quantopian.pipeline.filters import QTradableStocksUS
from quantopian.pipeline.factors import BusinessDaysSincePreviousEvent

# Import datasets which will be used
from quantopian.pipeline.data.factset.estimates import PeriodicConsensus, Actuals
from quantopian.pipeline.data.sentdex import sentiment

# import optimize
import quantopian.optimize as opt
 
# Import pandas
import pandas as pd

def initialize(context):
    """
    Initialize constants, create pipeline, and schedule functions
    This uses the default slippage and commission models
    """
    
    # Constants for min and max estimate surprise, sentiment, and days from announcement
    context.MIN_SHORT_SURPRISE = .025
    context.MAX_LONG_SURPRISE = -.025
    
    context.MIN_SHORT_SENTIMENT = 0.0
    context.MAX_LONG_SENTIMENT = 0.0
    
    context.MAX_DAYS_AFTER_EARNINGS_TO_OPEN = 3
    context.MAX_DAYS_AFTER_EARNINGS_TO_HOLD = 40
    
    # Constants for the min and max weights. for shorts these are negative
    context.MIN_WEIGHT = -.05
    context.MAX_WEIGHT = .05
    
    # Make our pipeline and attach to the algo
    attach_pipeline(make_pipeline(context), 'earnings_pipe')

    # Place orders
    schedule_function(
        func=place_orders,
        date_rule=date_rules.every_day(),
        time_rule=time_rules.market_open()
    )
    

def make_pipeline(context):
    """
    Define our pipeline.
    This not only fetches data but also implements the logic determining longs and shorts
    """
    
    # Create datasets of sales estimates and actuals for the most recent quarter (fq0).
    fq0_eps_cons = PeriodicConsensus.slice('EPS', 'qf', 0)
    fq0_eps_act = Actuals.slice('EPS', 'qf', 0)

    # Define factors of the last mean consensus EPS estimate and actual EPS.
    fq0_eps_cons_mean = fq0_eps_cons.mean.latest
    fq0_eps_act_value = fq0_eps_act.actual_value.latest

    # Define a surprise factor as the relative difference between the actual EPS and the final 
    # mean estimate made prior to the report being published. A positive value means the company
    # beat analyst expectations. A negative value means the company missed expectations.
    surprise = (fq0_eps_act_value - fq0_eps_cons_mean) / fq0_eps_cons_mean

    # Calculate the days since an earnings announcement impacted trading
    # The asof_date considers whether an announcement was before or after hours
    # We want to open on recent announcements but then close after an announcement is old
    days_since_announcement = BusinessDaysSincePreviousEvent(inputs=[fq0_eps_act.asof_date])
    recent_announcement = days_since_announcement <= context.MAX_DAYS_AFTER_EARNINGS_TO_OPEN
    old_announcement = days_since_announcement > context.MAX_DAYS_AFTER_EARNINGS_TO_HOLD

    # Define a sentiment factor.
    news_sentiment = sentiment.sentiment_signal.latest
        
    # Select short stocks which recently beat earnings and have a positive news sentiment. 
    # Likewise long stocks which recently missed earnings estimates and have a negative sentiment.
    shorts = (
        recent_announcement 
        & (surprise > context.MIN_SHORT_SURPRISE)
        & (news_sentiment > context.MIN_SHORT_SENTIMENT)
    )
    longs = (
        recent_announcement
        & (surprise < context.MAX_LONG_SURPRISE) 
        & (news_sentiment < context.MAX_LONG_SENTIMENT)
    )
    
    # Create our pipeline
    pipe = Pipeline(
        columns={
            'longs': longs,
            'shorts': shorts,
            'surprise': surprise,
            'old_announcement': old_announcement,
        },
        # Include only securities in the QTradableStocksUS universe
        screen=QTradableStocksUS(),
    )
    
    return pipe  
        
def before_trading_start(context, data):
    """
    Run our pipeline to fetch the actual data. 
    It's a good practice to place the pipeline execution here. 
    This gets allocated more time than scheduled functions.
    """
    
    context.output = pipeline_output('earnings_pipe')
    
def place_orders(context, data):
    """
    Use Optimize to place orders all at once
    """
    
    # get a list of the currently held securities.
    current_positions = list(context.portfolio.positions)

    # Make a series of the longs and shorts and associated alphas
    # Ensure that all the short alphas are negative (this is what tells opt to short them)
    # Also ensure the the long alphas are positive
    # Do this by inverting the surprise
    long_alphas = -context.output.query('index not in @current_positions and longs').surprise
    short_alphas = -context.output.query('index not in @current_positions and shorts').surprise

    # Hold any current positions which the announcement isn't old (again invert the surprise)
    hold_these_alphas = -context.output.query('index in @current_positions and not old_announcement').surprise
    
    # Combine the three
    all_alphas = pd.concat([long_alphas, short_alphas, hold_these_alphas])
    
    # Create our maximize alpha objective
    alpha_objective = opt.MaximizeAlpha(all_alphas) 
    
    # Set the constraints
    # At a minimun need a leverage constraint
    # also need to a limit on size of each position
    # and probably want longs and shorts to be equal (dollar_neutral)
    max_gross_exposure = opt.MaxGrossExposure(1.0)
    max_position_size = opt.PositionConcentration.with_equal_bounds(
        context.MIN_WEIGHT,
        context.MAX_WEIGHT
    )
    dollar_neutral = opt.DollarNeutral()
    
    # Execute the order_optimal_portfolio method with above objective and constraint
    # No need to loop through the stocks. 
    # The order_optimal_portfolio does all the ordering at one time
    # Also closes any positions not in 'all_alphas'
    # As a bonus also checks for 'can_trade'
    order_optimal_portfolio(
        objective = alpha_objective,
        constraints = [
            max_gross_exposure, 
            max_position_size, 
            dollar_neutral,
        ]
    )
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.

4 responses

Good work Saba. Easily written and quick.

I actually made a quick adjustment to the metrics that made a decent improvement. Thanks for making the algo easy to read and organized to do so.

Summary:
- Raised the beta by 0.01,
- Lowered the max drawdown by about 4%
- 4% Higher returns.
- Better sortino with SPY.

Made the adjustment all based on a twitter quote I saw years ago by a hedge fund manager. And he got the quote from another hedge fund manager even more years ago, "Amateurs trade before 10:30." Now taking that with a grain of salt and taking out the egotistical sentiment behind it, in practice, I've seen volatile trades happen in the first hour or so, and likely mean revert later on in the day. I at least believed this held true. I believe it definitely holds true in high volume large caps after an earnings jump. With a mean reversion volatility strategy like this, I felt it might actually make a quantifiable difference and appears as if so. Simply had the trading occur 2 hours and 29 minutes into the market open. (29 minutes because of maybe other algos out there trading at the 30 minute mark back then, because it's a nice round number). Here it is!

Note: May play around some more with extra changes, but here is a small change you can incorporate into this and many other algorithms to get seemingly better results, rather easily.

Clone Algorithm
47
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 example demonstrates how to use earnings data in your algorithm. The strategy is to short stocks that beat earnings and have a positive news sentiment. Likewise it long stocks which missed earnings estimates and have a negative sentiment. The amount ordered of each is based upon the earnings surprise. It closes positions after a fixed time since the announcement. This is a contrarian strategy which assumes traders over-react to news.
'''

# Import necessary Pipeline modules
from quantopian.pipeline import Pipeline
from quantopian.algorithm import attach_pipeline, pipeline_output

# Import specific filters and factors which will be used
from quantopian.pipeline.filters import QTradableStocksUS
from quantopian.pipeline.factors import BusinessDaysSincePreviousEvent

# Import datasets which will be used
from quantopian.pipeline.data.factset.estimates import PeriodicConsensus, Actuals
from quantopian.pipeline.data.sentdex import sentiment

# import optimize
import quantopian.optimize as opt
 
# Import pandas
import pandas as pd

def initialize(context):
    """
    Initialize constants, create pipeline, and schedule functions
    This uses the default slippage and commission models
    """
    
    # Constants for min and max estimate surprise, sentiment, and days from announcement
    context.MIN_SHORT_SURPRISE = .025
    context.MAX_LONG_SURPRISE = -.025
    
    context.MIN_SHORT_SENTIMENT = 0.0
    context.MAX_LONG_SENTIMENT = 0.0
    
    context.MAX_DAYS_AFTER_EARNINGS_TO_OPEN = 3
    context.MAX_DAYS_AFTER_EARNINGS_TO_HOLD = 40
    
    # Constants for the min and max weights. for shorts these are negative
    context.MIN_WEIGHT = -.05
    context.MAX_WEIGHT = .05
    
    # Make our pipeline and attach to the algo
    attach_pipeline(make_pipeline(context), 'earnings_pipe')

    # Place orders
    schedule_function(
        func=place_orders,
        date_rule=date_rules.every_day(),
        time_rule=time_rules.market_open(hours=2, minutes=29)
    )
    

def make_pipeline(context):
    """
    Define our pipeline.
    This not only fetches data but also implements the logic determining longs and shorts
    """
    
    # Create datasets of sales estimates and actuals for the most recent quarter (fq0).
    fq0_eps_cons = PeriodicConsensus.slice('EPS', 'qf', 0)
    fq0_eps_act = Actuals.slice('EPS', 'qf', 0)

    # Define factors of the last mean consensus EPS estimate and actual EPS.
    fq0_eps_cons_mean = fq0_eps_cons.mean.latest
    fq0_eps_act_value = fq0_eps_act.actual_value.latest

    # Define a surprise factor as the relative difference between the actual EPS and the final 
    # mean estimate made prior to the report being published. A positive value means the company
    # beat analyst expectations. A negative value means the company missed expectations.
    surprise = (fq0_eps_act_value - fq0_eps_cons_mean) / fq0_eps_cons_mean

    # Calculate the days since an earnings announcement impacted trading
    # The asof_date considers whether an announcement was before or after hours
    # We want to open on recent announcements but then close after an announcement is old
    days_since_announcement = BusinessDaysSincePreviousEvent(inputs=[fq0_eps_act.asof_date])
    recent_announcement = days_since_announcement <= context.MAX_DAYS_AFTER_EARNINGS_TO_OPEN
    old_announcement = days_since_announcement > context.MAX_DAYS_AFTER_EARNINGS_TO_HOLD

    # Define a sentiment factor.
    news_sentiment = sentiment.sentiment_signal.latest
        
    # Select short stocks which recently beat earnings and have a positive news sentiment. 
    # Likewise long stocks which recently missed earnings estimates and have a negative sentiment.
    shorts = (
        recent_announcement 
        & (surprise > context.MIN_SHORT_SURPRISE)
        & (news_sentiment > context.MIN_SHORT_SENTIMENT)
    )
    longs = (
        recent_announcement
        & (surprise < context.MAX_LONG_SURPRISE) 
        & (news_sentiment < context.MAX_LONG_SENTIMENT)
    )
    
    # Create our pipeline
    pipe = Pipeline(
        columns={
            'longs': longs,
            'shorts': shorts,
            'surprise': surprise,
            'old_announcement': old_announcement,
        },
        # Include only securities in the QTradableStocksUS universe
        screen=QTradableStocksUS(),
    )
    
    return pipe  
        
def before_trading_start(context, data):
    """
    Run our pipeline to fetch the actual data. 
    It's a good practice to place the pipeline execution here. 
    This gets allocated more time than scheduled functions.
    """
    
    context.output = pipeline_output('earnings_pipe')
    
def place_orders(context, data):
    """
    Use Optimize to place orders all at once
    """
    
    # get a list of the currently held securities.
    current_positions = list(context.portfolio.positions)

    # Make a series of the longs and shorts and associated alphas
    # Ensure that all the short alphas are negative (this is what tells opt to short them)
    # Also ensure the the long alphas are positive
    # Do this by inverting the surprise
    long_alphas = -context.output.query('index not in @current_positions and longs').surprise
    short_alphas = -context.output.query('index not in @current_positions and shorts').surprise

    # Hold any current positions which the announcement isn't old (again invert the surprise)
    hold_these_alphas = -context.output.query('index in @current_positions and not old_announcement').surprise
    
    # Combine the three
    all_alphas = pd.concat([long_alphas, short_alphas, hold_these_alphas])
    
    # Create our maximize alpha objective
    alpha_objective = opt.MaximizeAlpha(all_alphas) 
    
    # Set the constraints
    # At a minimun need a leverage constraint
    # also need to a limit on size of each position
    # and probably want longs and shorts to be equal (dollar_neutral)
    max_gross_exposure = opt.MaxGrossExposure(1.0)
    max_position_size = opt.PositionConcentration.with_equal_bounds(
        context.MIN_WEIGHT,
        context.MAX_WEIGHT
    )
    dollar_neutral = opt.DollarNeutral()
    
    # Execute the order_optimal_portfolio method with above objective and constraint
    # No need to loop through the stocks. 
    # The order_optimal_portfolio does all the ordering at one time
    # Also closes any positions not in 'all_alphas'
    # As a bonus also checks for 'can_trade'
    order_optimal_portfolio(
        objective = alpha_objective,
        constraints = [
            max_gross_exposure, 
            max_position_size, 
            dollar_neutral,
        ]
    )
There was a runtime error.

Saba your algo isn't usable in the contest as it is, it fails to pass many risk tests, such as sector exposure

Hi,

Is there a way to get FactSet Estimates data post 11/08/2018? In case that it's not possible, is there any substitute accessible through the Quantopian API?

I guess this restriction is part of Quantopian's agreements with the data provider (and that's why they provide data for free)?