Back to Community
Tech sector strategy with SPY hedge and earnings call avoidance

This is a simple strategy that rebalances a few large market cap tech stocks and hedges its positions with the SPY. It also exits out of these positions 1 day before an earnings announcement and re-enters the position 1 day afterwards.

I've written about the subject of earnings announcements and volatility. While you can read the full research based off a Stanford study, the quick summary is that stock price volatility on the day of an earnings announcement is significantly higher than its average.

To run the algorithm, get the free sample version of EventVestor's Earnings Calendar dataset and clone the algorithm below.

Would you like to see something like a tech-sector algorithm with Pipeline? Post in the comments.

Clone Algorithm
Loading...
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.factors.eventvestor 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.
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.

1 response

Here is the benchmark (the version of the algorithm w/o earnings calendars filtering).

For comparison, by accounting for earnings calendars the algorithm experienced these over the benchmark attached below:

alpha increased by 0.00308643136594, a 12.85% improvement  
returns increased by 0.0192049573895, a 41.37% improvement  
sharpe increased by 0.295107746787, a 71.54% improvement  
volatility decreased by 0.00114637616142, a 1.68% improvement  
max_drawdown increased by 0.00144116087525, a 2.60% decrease  
Clone Algorithm
Loading...
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.