Back to Community
Long/Short Earnings Sentiment Trading Strategy

7/15/2016 Update:

Access to the Estimize dataset will temporarily be shut down starting July 18th, 2016.

We've identified an issue with the manner in which we were processing the Estimize dataset that prevented updates to the data starting June, 2016. All subscribers have been notified and we are taking steps to implement a solution. 

The algorithm here has been updated to use analyst estimates from Zack's Earnings Surprises  

This is similar to a post-earnings announcement drift strategy I’ve published a few months ago that attempts to profit off the difference between reported earnings and earnings estimates based off a white paper by Vinesh Jha. Similar to that algorithm, I use crowdsourced earnings estimates as opposed to the Street’s consensus. The one crucial difference between this strategy and my previous one is that here, I use news sentiment to determine my long/short positions.

The idea is that we only trade on securities with a news sentiment that matches the direction of the earnings surprise. For example, only hold positions in securities with both a positive earnings surprise and a positive news sentiment. This also means that there are two crucial factors influence a trading decision after an earnings announcement and in my opinion, the new method provides a much more clear trading signal than earnings surprises alone.

The inspiration for this algorithm came from Steven Hayes, one of our community members.

Strategy Details:

  • Data set: Earnings calendar by EventVestor, crowdsourced earnings estimates by Estimize, and news sentiment by Accern
  • Weights: The weight for each security is determined by the total number of longs and shorts we have in that current day. So if we have 2 longs and 2 shorts, the weight for each long will be 50% (1.0/number of securities) and the weight for each short will be -50%. This is a rolling rebalance at the beginning of each day according to the number of securities currently held and to order.
  • Capital base: $1,000,000
  • Profit and Loss limits are set to 6%
  • Days held: Positions are currently held for 8 days but are easily changeable by modifying 'context.days_to_hold'
  • Percent threshold: Only surprises between 0% and 6% in absolute magnitude will be considered as a trading signal. These are adjustable using the minimum and maximum threshold variables in context.
  • Earnings dates: All trades are made 1 business day AFTER an earnings announcement regardless of whether it was a Before Market Open or After Market announcement

Notes Since Release

Also as an observation (as of June 21, 2016), I've been live trading
this example strategy for a few weeks now and it seems that in the
past 30~ days, it hasn't made any trades. From what I know it could be
for a few reasons:

  • The trading signal triggers are not being triggered by the earnings
    announcements (perhaps the percent limits are too strict)
  • Estimize's data coverage as compared to someone like Zack's is a
    lot smaller, so the mix of small coverage and the current time of year
    contribute to the lack of triggered trades

Overall, the backtested strategy had relatively few trades and I would
suggest more work is needed to make it robust enough to be holding
positions consistently as opposed to all cash (similar to what Lyth
has worked on).

With analyst earnings surprises from Zacks available soon in pipeline
and estimates to follow, it'll be interesting to see how the two will
compare as the analyst surprises are more robust w/ much wider coverage

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.

45 responses

Hi Seong,

I just want to clarify what I am reading.

The following part here:

On Long & Profit

if price >= cost_basis * 1.06 and current_position > 0:
order_target_percent(security, 0)
log.info( str(security) + ' Sold Long for Profit')
del context.stocks_held[security]

On Short & Profit
if price <= cost_basis* 0.94 and current_position < 0:
order_target_percent(security, 0)
log.info( str(security) + ' Sold Short for Profit')
del context.stocks_held[security]

The "cost_basis*.94" is the thing that triggers the 6% profit trigger.... Right? It is this way, not 1.06, because this is a short position.

So if I wanted to trigger every 3% I would just use the cost basis times the .97 for a short and the cost basis * 1.03 for the long.. right?

Thank you Seong for posting this.

Best regards,
Lovis

Yep! If you wanted to you could simplify that like so:

def check_positions_for_loss_or_profit(context, data):  
    # Sell our positions on longs/shorts for profit or loss  
    profit_loss_limit = .03  
    for security in context.portfolio.positions:  
        is_stock_held = context.stocks_held.get(security) >= 0  
        if data.can_trade(security) and is_stock_held and not get_open_orders(security):  
            current_position = context.portfolio.positions[security].amount  
            cost_basis = context.portfolio.positions[security].cost_basis  
            price = data.current(security, 'price')  
            # On Long & Profit  
            if price >= cost_basis * (1 + profit_loss_limit) and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Profit')  
                del context.stocks_held[security]  
            # On Short & Profit  
            if price <= cost_basis* (1 - profit_loss_limit)  and current_position < 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Profit')  
                del context.stocks_held[security]  
            # On Long & Loss  
            if price <= cost_basis * (1 - profit_loss_limit) and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Loss')  
                del context.stocks_held[security]  
            # On Short & Loss  
            if price >= cost_basis * (1 + profit_loss_limit)  and current_position < 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Loss')  
                del context.stocks_held[security]  

Seong,

Cool! Thanks!
One question, how would I keep a single position rather than compounding a position? What is the code for that?

Best regards,
Lovis

Lovis, I'm not quite sure what you're asking. Is it related to the algorithm above?

Here is an improved version that performs much better out-of-sample.

The days held is set to 4 and the loss limit is set to 3%

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 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
- https://www.quantopian.com/data/accern/alphaone

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.data.builtin import USEquityPricing
from quantopian.pipeline.factors import CustomFactor, AverageDollarVolume

# Premium version avilable at
# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.estimize import (
    ConsensusEstimizeEPS,
    ConsensusWallstreetEPS,
    ConsensusEstimizeRevenue, 
    ConsensusWallstreetRevenue
)

# 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
)

from quantopian.pipeline.data.accern import alphaone_free as alphaone
# Premium version availabe at
# https://www.quantopian.com/data/accern/alphaone
# from quantopian.pipeline.data.accern import alphaone as alphaone

class PercentSurprise(CustomFactor):
    window_length = 1
    inputs = [ConsensusEstimizeEPS.previous_actual_value,
              ConsensusEstimizeEPS.previous_mean]

    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()  

    # Instantiating our factors  
    factor = PercentSurprise()

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Filter down to stocks in the top/bottom  
    longs = (factor >= context.min_surprise) & (factor <= context.max_surprise)
    shorts = (factor <= -context.min_surprise) & (factor >= -context.max_surprise)

    # Set our pipeline screens  
    # Filter down stocks using sentiment  
    article_sentiment = alphaone.article_sentiment.latest
    top_universe = is_liquid & longs & article_sentiment.notnan() & (article_sentiment > .45)
    bottom_universe = is_liquid & shorts & article_sentiment.notnan() & (article_sentiment < -.45)

    # Add long/shorts to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(bottom_universe, "shorts")
    pipe.add(BusinessDaysSincePreviousEarnings(), 'pe')
    
    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 = 4
    #: 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 = .06
    
    #: Turn on/off stop/limit
    context.loss_limit = .03
    context.profit_limit = .06
    context.profit_and_loss_limit_on = True

    #: 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_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())

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]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]
    context.negative_surprise = assets_in_universe[results.shorts]

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        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
    record(leverage=context.account.leverage)

    # Check our positions for loss or profit and exit if necessary
    if context.profit_and_loss_limit_on:
        check_positions_for_loss_or_profit(context, data)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1  
                if context.stocks_held[security] >= context.days_to_hold:  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]  
            # If we've deleted it but it still hasn't been exited. Try exiting again  
            else:  
                log.info("Haven't yet exited %s, ordering again" % security.symbol)  
                order_target_percent(security, 0)  

    # Check our current 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.tolist() + current_negative_pos
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos
    
    # Rebalance our negative surprise securities (existing + new)
    for security in negative_stocks:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            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:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            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.current(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 check_positions_for_loss_or_profit(context, data):
    # Sell our positions on longs/shorts for profit or loss
    for security in context.portfolio.positions:
        is_stock_held = context.stocks_held.get(security) >= 0
        if data.can_trade(security) and is_stock_held and not get_open_orders(security):
            current_position = context.portfolio.positions[security].amount  
            cost_basis = context.portfolio.positions[security].cost_basis  
            price = data.current(security, 'price')
            # On Long & Profit
            if price >= cost_basis * (1 + context.profit_limit) and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Profit')  
                del context.stocks_held[security]  
            # On Short & Profit
            if price <= cost_basis * (1 - context.profit_limit) and current_position < 0:
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Profit')  
                del context.stocks_held[security]
            # On Long & Loss
            if price <= cost_basis * (1 - context.loss_limit) and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Loss')  
                del context.stocks_held[security]  
            # On Short & Loss
            if price >= cost_basis * (1 + context.loss_limit) and current_position < 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Loss')  
                del context.stocks_held[security]  
There was a runtime error.

Hi Seong

I took your version and converted it to Long only and saw after adding PvR code that it was shorting and going into neg cash. I implemented Order Tracking, PvR and Queues into this and it no longer holds any neg cash and leverage never goes above 1.

Credit goes to garyha on creating the PvR code, Order Tracking and Queues in other Algos.

PvR - https://www.quantopian.com/posts/pvr-profit-vs-risk

Order Tracking - https://www.quantopian.com/posts/track-orders

Queue Logic from garyha's backtest - https://www.quantopian.com/posts/minimum-variance-w-slash-constraint#56b9683cc3c398a1c200053e

Please let me know what you think and if you can improve further as well. This is looking like a good strategy.

I changed the sell profit and loss limit to 5%, days to hold to 5.

Clone Algorithm
528
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 pytz import timezone       # Python only does once, makes this portable.  
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, AverageDollarVolume

# Premium version avilable at
# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.estimize import (
    ConsensusEstimizeEPS,
    ConsensusWallstreetEPS,
    ConsensusEstimizeRevenue, 
    ConsensusWallstreetRevenue
)

# 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
)

from quantopian.pipeline.data.accern import alphaone as alphaone
# Premium version availabe at
# https://www.quantopian.com/data/accern/alphaone
# from quantopian.pipeline.data.accern import alphaone as alphaone

class PercentSurprise(CustomFactor):
    window_length = 1
    inputs = [ConsensusEstimizeEPS.previous_actual_value,
              ConsensusEstimizeEPS.previous_mean]

    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()  

    # Instantiating our factors  
    factor = PercentSurprise()

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Filter down to stocks in the top/bottom  
    longs = (factor >= context.min_surprise) & (factor <= context.max_surprise)

    # Set our pipeline screens  
    # Filter down stocks using sentiment  
    article_sentiment = alphaone.article_sentiment.latest
    top_universe = is_liquid & longs & article_sentiment.notnan() & (article_sentiment > .45)

    # Add longs to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(BusinessDaysSincePreviousEarnings(), 'pe')
    
    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))
    
    context.queue_list = []
    context.track_orders = 1    # toggle on|off
    
    context.orders = {}               # Move these to initialize() for better efficiency.  
    context.dates  = { 
            'active': 0,  
            'start' : [],           # Start dates, option  
            'stop'  : []            # Stop  dates, option  
        }  
    
    #: Set to Long Only
    set_long_only()

    #: Declaring the days to hold, change this to what you want)))
    context.days_to_hold = 5
    #: 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 = .06

    # 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_close())
    # Order our positions
    schedule_function(func=queues,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())
    '''
    # For every minute available (max is 6 hours and 30 minutes)
    total_minutes = 6*60 + 30

    for i in range(total_minutes):
    # Every 30 minutes run schedule
        if i % 30 == 0:
            schedule_function(check_positions_for_loss_or_profit,
                              date_rules.every_day(),
                              time_rules.market_open(minutes=i),
                              True)
    '''

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]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]


def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        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 trade(context, data):    # Process any queued orders
    if get_open_orders(): return    # Wait for fills

    c = context
    mult = .95        # Multiplier for weights in orders, cash vs
                       #  slippage, commissions, to avoid negative cash.
    log_changes = 1    # Whether to log weight|allocation changes.
    sells = 0          # Indicator, whether any sells happened.
    qlist = sorted(c.queue_list)[:]  # Make an independent copy to allow remove().

    for o in qlist:    # Each order queued, process any sells
        security  = o[1] ; weight = o[0]
        pf_value_now = c.portfolio.positions[security].amount * data.current(security, 'price')
        pf_ratio_now = pf_value_now / c.portfolio.portfolio_value
        if weight < pf_ratio_now:   # sell, is decrease in allocation
            if log_changes:
                log.info('   {} {} {} ==> {}'.format(
                    minut(), security.symbol, '%.3f' % pf_ratio_now, '%.3f' % weight))
            order_target_percent(security, mult * weight)    # Selling
            c.queue_list.remove(o)
            sells = 1  # let these settle before/if any buys

    if sells: return   # let any sells go thru before buys

    '''
    To do if Robinhood: Make sure T+3 is satisfied before buys here.
    Untested ...
    c = context
    date = get_datetime().date()
    if c.date_prv != date:
        c.day_count += 1
        c.date_prv = date
    if c.day_count <= 3:
        return
    else:
        c.day_count = 0
    '''

    for o in qlist:    # Should be all buys at this point
        security  = o[1] ; weight = o[0]
        pf_value_now = c.portfolio.positions[security].amount * data.current(security, 'price')
        pf_ratio_now = pf_value_now / c.portfolio.portfolio_value
        if weight > pf_ratio_now:   # buy, is increase in allocation
            if log_changes:
                log.info('   {} {} {} ==> {}'.format(
                    minut(), security.symbol, '%.3f' % pf_ratio_now, '%.3f' % weight))
            order_target_percent(security, mult * weight)    # Buying
            c.queue_list.remove(o)
        
 
def queues(context, data): # Was Order_Positions Before
    """
    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.
    """
    if context.queue_list: return    # wait for orders to clear

    port = context.portfolio.positions
    #record(leverage=context.account.leverage)
    
    # Check our positions for loss or profit and exit if necessary
    check_positions_for_loss_or_profit(context, data)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    sell_allocation = 0
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1  
                if context.stocks_held[security] >= context.days_to_hold:  
                    context.queue_list.append((sell_allocation, security))   
                    del context.stocks_held[security]
            # If we've deleted it but it still hasn't been exited. Try exiting again  
          #  else:  
          #      log.info("Haven't yet exited %s, ordering again" % security.symbol)  
          #      order_target_percent(security, 0)  

    # Check our current positions
    current_positive_pos = [pos for pos in port if (port[pos].amount > 0 and pos in context.stocks_held)]
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos
    
    # Rebalance our positive surprise securities (existing + new)                
    for security in positive_stocks:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            buy_allocation = 0.95 / len(positive_stocks)
            context.queue_list.append((buy_allocation, security))
            if context.stocks_held.get(security) is None:
                context.stocks_held[security] = 0
    

def check_positions_for_loss_or_profit(context, data):
    # Sell our positions on longs for profit or loss
    if context.queue_list: return    # wait for orders to clear
    sell_allocation = 0
    for security in context.portfolio.positions:
        is_stock_held = context.stocks_held.get(security) > 0
        if data.can_trade(security) and is_stock_held and not get_open_orders(security):
            current_position = context.portfolio.positions[security].amount  
            cost_basis = context.portfolio.positions[security].cost_basis  
            price = data.current(security, 'price')
            # On Long & Profit
            if price >= cost_basis * 1.04 and current_position > 0:  
                context.queue_list.append((sell_allocation, security))  
                log.info( str(security) + ' Sold Long for Profit')  
                del context.stocks_held[security]  
            # On Long & Loss
            if price <= cost_basis * 0.95 and current_position > 0:  
                context.queue_list.append((sell_allocation, security))
                log.info( str(security) + ' Sold Long for Loss')  
                del context.stocks_held[security]
    track_orders(context, data)

def handle_data(context, data):
    if get_open_orders():
        track_orders(context, data)    # for filled orders
        return
    if context.queue_list:
        trade(context, data)
    track_orders(context, data)        # for new orders and last-frame-filled's
    #check_positions_for_loss_or_profit(context, data)
    pvr(context, data)    
    
def track_orders(context, data):  # Log orders created, filled, unfilled or canceled.  
    '''      https://www.quantopian.com/posts/track-orders  
    Status:  
       0 - Unfilled  
       1 - Filled (can be partial)  
       2 - Canceled  
    '''  
    c = context  
    log_cash = 1    # Show cash values in logging window or not.  
    log_ids  = 1    # Include order id's in logging window or not.

    ''' Start and stop date options ...  
    To not overwhelm the logging window, start/stop dates can be entered  
      either below or in initialize() if you move to there for better efficiency.  
    Example:  
        c.dates  = {  
            'active': 0,  
            'start' : ['2007-05-07', '2010-04-26'],  
            'stop'  : ['2008-02-13', '2010-11-15']  
        }  
    '''  
    '''
    if 'orders' not in c:  
        c.orders = {}               # Move these to initialize() for better efficiency.  
        c.dates  = { 
            'active': 0,  
            'start' : [],           # Start dates, option  
            'stop'  : []            # Stop  dates, option  
        }  
    '''

    # If the dates 'start' or 'stop' lists have something in them, sets them.  
    if c.dates['start'] or c.dates['stop']:  
        date = str(get_datetime().date())
        if   date in c.dates['start']:    # See if there's a match to start  
            c.dates['active'] = 1  
        elif date in c.dates['stop']:     #   ... or to stop  
            c.dates['active'] = 0  
    else:  
        c.dates['active'] = 1  # Set to active b/c no conditions

    if c.dates['active'] == 0:  
        return                 # Skip if off

    def _minute():   # To preface each line with the minute of the day.  
        if get_environment('data_frequency') == 'minute':  
            bar_dt = get_datetime().astimezone(timezone('US/Eastern'))  
            minute = (bar_dt.hour * 60) + bar_dt.minute - 570  # (-570 = 9:31a)  
            return str(minute).rjust(3)  
        return ''    # Daily mode, just leave it out.

    def _orders(to_log):    # So all logging comes from the same line number,  
        log.info(to_log)    #   for vertical alignment in the logging window.

    ordrs = c.orders.copy()    # Independent copy to allow deletes  
    for id in ordrs:  
        o    = get_order(id)  
        sec  = o.sid ; sym = sec.symbol  
        oid  = o.id if log_ids else ''  
        cash = 'cash {}'.format(int(c.portfolio.cash)) if log_cash else ''  
        prc  = '%.2f' % data.current(sec, 'price')
        if o.filled:        # Filled at least some  
            trade  = 'Bot' if o.amount > 0 else 'Sold'  
            filled = '{}'.format(o.amount)  
            if o.filled == o.amount:    # complete  
                if 0 < c.orders[o.id] < o.amount:  
                    filled  = 'all/{}'.format(o.amount)  
                del c.orders[o.id]  
            else:  
                done_prv       = c.orders[o.id]       # previously filled ttl  
                filled_this    = o.filled - done_prv  # filled this time, can be 0  
                c.orders[o.id] = o.filled             # save for increments math  
                filled         = '{}/{}'.format(filled_this, o.amount)  
            _orders(' {}      {} {} {} at {}   {} {}'.format(_minute(),  
                trade, filled, sym, prc, cash, oid))  
        else:  
            canceled = 'canceled' if o.status == 2 else ''  
            _orders(' {}         {} {} unfilled {} {}'.format(_minute(),  
                    o.sid.symbol, o.amount, canceled, oid))  
            if canceled: del c.orders[o.id]

    for oo_list in get_open_orders().values(): # Open orders list  
        for o in oo_list:  
            sec  = o.sid ; sym = sec.symbol  
            oid  = o.id if log_ids else ''  
            cash = 'cash {}'.format(int(c.portfolio.cash)) if log_cash else ''  
            prc  = '%.2f' % data.current(sec, 'price')
            if o.status == 2:                  # Canceled  
                _orders(' {}    Canceled {} {} order   {} {}'.format(_minute(),  
                        trade, o.amount, sym, prc, cash, oid))  
                del c.orders[o.id]  
            elif o.id not in c.orders:         # New  
                c.orders[o.id] = 0  
                trade = 'Buy' if o.amount > 0 else 'Sell'  
                if o.limit:                    # Limit order  
                    _orders(' {}   {} {} {} now {} limit {}   {} {}'.format(_minute(),  
                        trade, o.amount, sym, prc, o.limit, cash, oid))  
                elif o.stop:                   # Stop order  
                    _orders(' {}   {} {} {} now {} stop {}   {} {}'.format(_minute(),  
                        trade, o.amount, sym, prc, o.stop, cash, oid))  
                else:                          # Market order  
                    _orders(' {}   {} {} {} at {}   {} {}'.format(_minute(),  
                        trade, o.amount, sym, prc, cash, oid))  
                
def pvr(context, data):  
    ''' Custom chart and/or log of profit_vs_risk returns and related information  
    '''  
    # # # # # # # # # #  Options  # # # # # # # # # #  
    record_max_lvrg = 1         # Maximum leverage encountered  
    record_leverage = 0         # Leverage (context.account.leverage)  
    record_q_return = 0         # Quantopian returns (percentage)  
    record_pvr      = 1         # Profit vs Risk returns (percentage)  
    record_pnl      = 1         # Profit-n-Loss  
    record_shorting = 0         # Total value of any shorts  
    record_overshrt = 0         # Shorts beyond longs+cash  
    record_risk     = 0         # Risked, max cash spent or shorts beyond longs+cash  
    record_risk_hi  = 1         # Highest risk overall  
    record_cash     = 0         # Cash available  
    record_cash_low = 1         # Any new lowest cash level  
    logging         = 1         # Also to logging window conditionally (1) or not (0)  
    log_method      = 'risk_hi' # 'daily' or 'risk_hi'

    from pytz import timezone   # Python will only do once, makes this portable.  
                                #   Move to top of algo for better efficiency.  
    c = context  # Brevity is the soul of wit -- Shakespeare [for efficiency, readability]  
    if 'pvr' not in c:  
        date_strt = get_environment('start').date()  
        date_end  = get_environment('end').date()  
        cash_low  = c.portfolio.starting_cash  
        mode      = get_environment('data_frequency')  
        c.pvr = {  
            'max_lvrg': 0,  
            'risk_hi' : 0,  
            'days'    : 0.0,  
            'date_prv': '',  
            'cash_low': cash_low,  
            'date_end': date_end,  
            'mode'    : mode,  
            'run_str' : '{} to {}  {}  {}'.format(date_strt,date_end,int(cash_low),mode)  
        }  
        log.info(c.pvr['run_str'])  
    pvr_rtrn     = 0            # Profit vs Risk returns based on maximum spent  
    profit_loss  = 0            # Profit-n-loss  
    shorts       = 0            # Shorts value  
    longs        = 0            # Longs  value  
    overshorts   = 0            # Shorts value beyond longs plus cash  
    new_risk_hi  = 0  
    new_cash_low = 0                           # To trigger logging in cash_low case  
    lvrg         = c.account.leverage          # Standard leverage, in-house  
    date         = get_datetime().date()       # To trigger logging in daily case  
    cash         = c.portfolio.cash  
    start        = c.portfolio.starting_cash  
    cash_dip     = int(max(0, start - cash))  
    q_rtrn       = 100 * (c.portfolio.portfolio_value - start) / start

    if int(cash) < c.pvr['cash_low']:                # New cash low  
        new_cash_low = 1  
        c.pvr['cash_low']   = int(cash)  
        if record_cash_low:  
            record(CashLow = int(c.pvr['cash_low'])) # Lowest cash level hit

    if record_max_lvrg:  
        if c.account.leverage > c.pvr['max_lvrg']:  
            c.pvr['max_lvrg'] = c.account.leverage  
            record(MaxLv = c.pvr['max_lvrg'])        # Maximum leverage

    if record_pnl:  
        profit_loss = c.portfolio.pnl  
        record(PnL = profit_loss)                    # "Profit and Loss" in dollars

    for p in c.portfolio.positions:
        if data.can_trade(p):
            shrs = c.portfolio.positions[p].amount 
            if shrs < 0:  
                shorts += int(abs(shrs * data.current(p, 'price')))
            if shrs > 0:  
                longs  += int(shrs * data.current(p, 'price'))

    if shorts > longs + cash: overshorts = shorts             # Shorts when too high  
    if record_shorting: record(Shorts  = shorts)              # Shorts value as a positve  
    if record_overshrt: record(OvrShrt = overshorts)          # Shorts value as a positve  
    if record_cash:     record(Cash = int(c.portfolio.cash))  # Cash  
    if record_leverage: record(Lvrg = c.account.leverage)     # Leverage

    risk = int(max(cash_dip, shorts))  
    if record_risk: record(Risk = risk)       # Amount in play, maximum of shorts or cash used

    if risk > c.pvr['risk_hi']:  
        c.pvr['risk_hi'] = risk  
        new_risk_hi = 1

        if record_risk_hi:  
            record(RiskHi = c.pvr['risk_hi']) # Highest risk overall

    if record_pvr:      # Profit_vs_Risk returns based on max amount actually spent (risk high)  
        if c.pvr['risk_hi'] != 0:     # Avoid zero-divide  
            pvr_rtrn = 100 * (c.portfolio.portfolio_value - start) / c.pvr['risk_hi']  
            record(PvR = pvr_rtrn)            # Profit_vs_Risk returns

    if record_q_return:  
        record(QRet = q_rtrn)                 # Quantopian returns to compare to pvr returns curve

    def _minute():   # To preface each line with minute of the day.  
        if get_environment('data_frequency') == 'minute':  
            bar_dt = get_datetime().astimezone(timezone('US/Eastern'))  
            minute = (bar_dt.hour * 60) + bar_dt.minute - 570  # (-570 = 9:31a)  
            return str(minute).rjust(3)  
        return ''    # Daily mode, just leave it out.

    def _pvr_():  
            log.info('PvR {} %/day     {}'.format(  
                '%.4f' % (pvr_rtrn / c.pvr['days']), c.pvr['run_str']))  
            log.info('  Profited {} on {} activated/transacted for PvR of {}%'.format(  
                '%.0f' % (c.portfolio.portfolio_value - start), '%.0f' % c.pvr['risk_hi'],  
                '%.1f' % pvr_rtrn))  
            log.info('  QRet {} PvR {} CshLw {} MxLv {} RskHi {} Shrts {}'.format(  
                '%.2f' % q_rtrn, '%.2f' % pvr_rtrn, '%.0f' % c.pvr['cash_low'],  
                '%.2f' % c.pvr['max_lvrg'], '%.0f' % c.pvr['risk_hi'], '%.0f' % shorts))

    if logging:  
        if log_method == 'risk_hi' and new_risk_hi \
          or log_method == 'daily' and c.pvr['date_prv'] != date \
          or new_cash_low:  
            qret    = ' QRet '   + '%.1f' % q_rtrn  
            lv      = ' Lv '     + '%.1f' % lvrg              if record_leverage else ''  
            pvr     = ' PvR '    + '%.1f' % pvr_rtrn          if record_pvr      else ''  
            pnl     = ' PnL '    + '%.0f' % profit_loss       if record_pnl      else ''  
            csh     = ' Cash '   + '%.0f' % cash              if record_cash     else ''  
            shrt    = ' Shrt '   + '%.0f' % shorts            if record_shorting else ''  
            ovrshrt = ' Shrt '   + '%.0f' % overshorts        if record_overshrt else ''  
            risk    = ' Risk '   + '%.0f' % risk              if record_risk     else ''  
            mxlv    = ' MaxLv '  + '%.2f' % c.pvr['max_lvrg'] if record_max_lvrg else ''  
            csh_lw  = ' CshLw '  + '%.0f' % c.pvr['cash_low'] if record_cash_low else ''  
            rsk_hi  = ' RskHi '  + '%.0f' % c.pvr['risk_hi']  if record_risk_hi  else ''  
            log.info('{}{}{}{}{}{}{}{}{}{}{}{}'.format(_minute(),  
               lv, mxlv, qret, pvr, pnl, csh, csh_lw, shrt, ovrshrt, risk, rsk_hi))  
    if c.pvr['date_prv'] != date: c.pvr['days'] += 1.0  
    if c.pvr['days'] % 130 == 0 and _minute() == '100': _pvr_()  
    c.pvr['date_prv'] = date  
    if c.pvr['date_end'] == date:  
        # Summary on last minute of last day.  
        # If using schedule_function(), backtest last day/time may need to match for this to execute.  
        if 'pvr_summary_done' not in c: c.pvr_summary_done = 0  
        log_summary = 0  
        if c.pvr['mode'] == 'daily' and get_datetime().date() == c.pvr['date_end']:  
            log_summary = 1  
        elif c.pvr['mode'] == 'minute' and get_datetime() == get_environment('end'):  
            log_summary = 1  
        if log_summary and not c.pvr_summary_done:  
            _pvr_()  
            c.pvr_summary_done = 1
def minut():   # To preface each line with the minute of the day.
               # Added to be used in trade()
    if get_environment('data_frequency') == 'minute':
        bar_dt = get_datetime().astimezone(timezone('US/Eastern'))
        minute = (bar_dt.hour * 60) + bar_dt.minute - 570  # (-570 = 9:31a)
        return str(minute).rjust(3)
    return ''    # Daily mode, just leave it out.

There was a runtime error.

Whoa Steven!

That is pretty tremendous!

Here's a different version that only looks at earnings surprises with Estimize Consensus Estimates that are relevant for the previous quarter.

The factor I use to determine that is here:

class DaysSinceRelease(CustomFactor):  
    # Only getting the previous quarter's estimize surprise  
    window_length = 1  
    inputs = [EarningsCalendar.previous_announcement,  
              ConsensusEstimizeEPS.previous_release_date]  
    def compute(self, today, assets, out,  
                earnings_announcement, estimize_release):  
        days = estimize_release - earnings_announcement  
        out[:] = abs(days.astype('timedelta64[D]').astype(int))  
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 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.data.builtin import USEquityPricing
from quantopian.pipeline.factors import CustomFactor, AverageDollarVolume

# Premium version avilable at
# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.estimize import (
    ConsensusEstimizeEPS,
    ConsensusWallstreetEPS,
    ConsensusEstimizeRevenue, 
    ConsensusWallstreetRevenue
)

# 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
)

from quantopian.pipeline.data.accern import alphaone_free as alphaone
# Premium version availabe at
# https://www.quantopian.com/data/accern/alphaone
# from quantopian.pipeline.data.accern import alphaone as alphaone

class PercentSurprise(CustomFactor):
    window_length = 1
    inputs = [ConsensusEstimizeEPS.previous_actual_value,
              ConsensusEstimizeEPS.previous_mean]

    def compute(self, today, assets, out, actual_eps, estimize_eps):
        out[:] = (actual_eps[-1] - estimize_eps[-1])/(estimize_eps[-1] + 0)
        
class DaysSinceRelease(CustomFactor):
    # Only getting the previous quarter's estimize surprise
    window_length = 1
    inputs = [EarningsCalendar.previous_announcement,
              ConsensusEstimizeEPS.previous_release_date]
    
    def compute(self, today, assets, out,
                earnings_announcement, estimize_release):
        days = estimize_release - earnings_announcement
        out[:] = abs(days.astype('timedelta64[D]').astype(int))

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

    # Instantiating our factors  
    factor = PercentSurprise()
    
    # Get the days since estimize release date and the latest
    # Earnings release date
    days_since_release = DaysSinceRelease()
    previous_estimize_release_date = \
        ConsensusEstimizeEPS.previous_release_date.latest

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Filter down to stocks in the top/bottom according to
    # the earnings surprise
    longs = (factor >= context.min_surprise) & (factor <= context.max_surprise)
    shorts = (factor <= -context.min_surprise) & (factor >= -context.max_surprise)

    # Set our pipeline screens  
    # Filter down stocks using sentiment  
    article_sentiment = alphaone.article_sentiment.latest
    top_universe = is_liquid & longs & article_sentiment.notnan() \
        & (article_sentiment > .45)
    bottom_universe = is_liquid & shorts & article_sentiment.notnan() \
        & (article_sentiment < -.45)

    # Add long/shorts to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(bottom_universe, "shorts")
    pipe.add(BusinessDaysSincePreviousEarnings(), 'pe')
    pipe.set_screen((days_since_release <= 7) &
                    (previous_estimize_release_date.notnull()))
    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 = 4
    #: 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 = .06

    #: 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_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())

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]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]
    context.negative_surprise = assets_in_universe[results.shorts]

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        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
    record(leverage=context.account.leverage)

    # Check our positions for loss or profit and exit if necessary
    check_positions_for_loss_or_profit(context, data)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1  
                if context.stocks_held[security] >= context.days_to_hold:  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]  
            # If we've deleted it but it still hasn't been exited. Try exiting again  
            else:  
                log.info("Haven't yet exited %s, ordering again" % security.symbol)  
                order_target_percent(security, 0)  

    # Check our current 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.tolist() + current_negative_pos
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos
    
    # Rebalance our negative surprise securities (existing + new)
    for security in negative_stocks:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            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:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            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.current(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 check_positions_for_loss_or_profit(context, data):
    # Sell our positions on longs/shorts for profit or loss
    for security in context.portfolio.positions:
        is_stock_held = context.stocks_held.get(security) >= 0
        if data.can_trade(security) and is_stock_held and not get_open_orders(security):
            current_position = context.portfolio.positions[security].amount  
            cost_basis = context.portfolio.positions[security].cost_basis  
            price = data.current(security, 'price')
            # On Long & Profit
            if price >= cost_basis * 1.06 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Profit')  
                del context.stocks_held[security]  
            # On Short & Profit
            if price <= cost_basis* 0.94 and current_position < 0:
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Profit')  
                del context.stocks_held[security]
            # On Long & Loss
            if price <= cost_basis * 0.94 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Loss')  
                del context.stocks_held[security]  
            # On Short & Loss
            if price >= cost_basis * 1.06 and current_position < 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Loss')  
                del context.stocks_held[security]  
There was a runtime error.

@Seong - this is a great algo! Good work! However, as I mentioned in my previous posts on some of your other algos, there is a problem with liquidity. Some of the tickers the algo trades would be too illiquid to trade. Is there a way to limit the algo to a certain volume? Aka, 1mm or more?

@Seong - this is a great algo! Good work! However, as I mentioned in my previous posts on some of your other algos, there is a problem with liquidity. Some of the tickers the algo trades would be too illiquid to trade. Is there a way to limit the algo to a certain volume? Aka, 1mm or more?

@Steven - the algo looks pretty impressive! I did run a backtest and a huge bump in returns around 2015/Feb/05 - 2015/Feb/06 got me curious, but I don't quite get what happened there.

Analysing portfolio/transactions around those dates I have discovered that majority of that profit comes from selling 256 shares of HBI on Feb 6th, even though portfolio had only 64 shares the day before.

@Seong does it look like a bug to you?

Clone Algorithm
24
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 pytz import timezone       # Python only does once, makes this portable.  
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, AverageDollarVolume

# Premium version avilable at
# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.estimize import (
    ConsensusEstimizeEPS,
    ConsensusWallstreetEPS,
    ConsensusEstimizeRevenue, 
    ConsensusWallstreetRevenue
)

# 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
)

from quantopian.pipeline.data.accern import alphaone as alphaone
# Premium version availabe at
# https://www.quantopian.com/data/accern/alphaone
# from quantopian.pipeline.data.accern import alphaone as alphaone

class PercentSurprise(CustomFactor):
    window_length = 1
    inputs = [ConsensusEstimizeEPS.previous_actual_value,
              ConsensusEstimizeEPS.previous_mean]

    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()  

    # Instantiating our factors  
    factor = PercentSurprise()

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Filter down to stocks in the top/bottom  
    longs = (factor >= context.min_surprise) & (factor <= context.max_surprise)

    # Set our pipeline screens  
    # Filter down stocks using sentiment  
    article_sentiment = alphaone.article_sentiment.latest
    top_universe = is_liquid & longs & article_sentiment.notnan() & (article_sentiment > .45)

    # Add longs to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(BusinessDaysSincePreviousEarnings(), 'pe')
    
    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))
    
    context.queue_list = []
    context.track_orders = 1    # toggle on|off
    
    context.orders = {}               # Move these to initialize() for better efficiency.  
    context.dates  = { 
            'active': 0,  
            'start' : [],           # Start dates, option  
            'stop'  : []            # Stop  dates, option  
        }  
    
    #: Set to Long Only
    set_long_only()

    #: Declaring the days to hold, change this to what you want)))
    context.days_to_hold = 5
    #: 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 = .06

    # 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_close())
    # Order our positions
    schedule_function(func=queues,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())
    '''
    # For every minute available (max is 6 hours and 30 minutes)
    total_minutes = 6*60 + 30

    for i in range(total_minutes):
    # Every 30 minutes run schedule
        if i % 30 == 0:
            schedule_function(check_positions_for_loss_or_profit,
                              date_rules.every_day(),
                              time_rules.market_open(minutes=i),
                              True)
    '''

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]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]


def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        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 trade(context, data):    # Process any queued orders
    if get_open_orders(): return    # Wait for fills

    c = context
    mult = .95        # Multiplier for weights in orders, cash vs
                       #  slippage, commissions, to avoid negative cash.
    log_changes = 1    # Whether to log weight|allocation changes.
    sells = 0          # Indicator, whether any sells happened.
    qlist = sorted(c.queue_list)[:]  # Make an independent copy to allow remove().

    for o in qlist:    # Each order queued, process any sells
        security  = o[1] ; weight = o[0]
        pf_value_now = c.portfolio.positions[security].amount * data.current(security, 'price')
        pf_ratio_now = pf_value_now / c.portfolio.portfolio_value
        if weight < pf_ratio_now:   # sell, is decrease in allocation
            if log_changes:
                log.info('   {} {} {} ==> {}'.format(
                    minut(), security.symbol, '%.3f' % pf_ratio_now, '%.3f' % weight))
            order_target_percent(security, mult * weight)    # Selling
            c.queue_list.remove(o)
            sells = 1  # let these settle before/if any buys

    if sells: return   # let any sells go thru before buys

    '''
    To do if Robinhood: Make sure T+3 is satisfied before buys here.
    Untested ...
    c = context
    date = get_datetime().date()
    if c.date_prv != date:
        c.day_count += 1
        c.date_prv = date
    if c.day_count <= 3:
        return
    else:
        c.day_count = 0
    '''

    for o in qlist:    # Should be all buys at this point
        security  = o[1] ; weight = o[0]
        pf_value_now = c.portfolio.positions[security].amount * data.current(security, 'price')
        pf_ratio_now = pf_value_now / c.portfolio.portfolio_value
        if weight > pf_ratio_now:   # buy, is increase in allocation
            if log_changes:
                log.info('   {} {} {} ==> {}'.format(
                    minut(), security.symbol, '%.3f' % pf_ratio_now, '%.3f' % weight))
            order_target_percent(security, mult * weight)    # Buying
            c.queue_list.remove(o)
        
 
def queues(context, data): # Was Order_Positions Before
    """
    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.
    """
    if context.queue_list: return    # wait for orders to clear

    port = context.portfolio.positions
    #record(leverage=context.account.leverage)
    
    # Check our positions for loss or profit and exit if necessary
    check_positions_for_loss_or_profit(context, data)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    sell_allocation = 0
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1  
                if context.stocks_held[security] >= context.days_to_hold:  
                    context.queue_list.append((sell_allocation, security))   
                    del context.stocks_held[security]
            # If we've deleted it but it still hasn't been exited. Try exiting again  
          #  else:  
          #      log.info("Haven't yet exited %s, ordering again" % security.symbol)  
          #      order_target_percent(security, 0)  

    # Check our current positions
    current_positive_pos = [pos for pos in port if (port[pos].amount > 0 and pos in context.stocks_held)]
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos
    
    # Rebalance our positive surprise securities (existing + new)                
    for security in positive_stocks:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            buy_allocation = 0.95 / len(positive_stocks)
            context.queue_list.append((buy_allocation, security))
            if context.stocks_held.get(security) is None:
                context.stocks_held[security] = 0
    

def check_positions_for_loss_or_profit(context, data):
    # Sell our positions on longs for profit or loss
    if context.queue_list: return    # wait for orders to clear
    sell_allocation = 0
    for security in context.portfolio.positions:
        is_stock_held = context.stocks_held.get(security) > 0
        if data.can_trade(security) and is_stock_held and not get_open_orders(security):
            current_position = context.portfolio.positions[security].amount  
            cost_basis = context.portfolio.positions[security].cost_basis  
            price = data.current(security, 'price')
            # On Long & Profit
            if price >= cost_basis * 1.04 and current_position > 0:  
                context.queue_list.append((sell_allocation, security))  
                log.info( str(security) + ' Sold Long for Profit')  
                del context.stocks_held[security]  
            # On Long & Loss
            if price <= cost_basis * 0.95 and current_position > 0:  
                context.queue_list.append((sell_allocation, security))
                log.info( str(security) + ' Sold Long for Loss')  
                del context.stocks_held[security]
    track_orders(context, data)

def handle_data(context, data):
    if get_open_orders():
        track_orders(context, data)    # for filled orders
        return
    if context.queue_list:
        trade(context, data)
    track_orders(context, data)        # for new orders and last-frame-filled's
    #check_positions_for_loss_or_profit(context, data)
    pvr(context, data)    
    
def track_orders(context, data):  # Log orders created, filled, unfilled or canceled.  
    '''      https://www.quantopian.com/posts/track-orders  
    Status:  
       0 - Unfilled  
       1 - Filled (can be partial)  
       2 - Canceled  
    '''  
    c = context  
    log_cash = 1    # Show cash values in logging window or not.  
    log_ids  = 1    # Include order id's in logging window or not.

    ''' Start and stop date options ...  
    To not overwhelm the logging window, start/stop dates can be entered  
      either below or in initialize() if you move to there for better efficiency.  
    Example:  
        c.dates  = {  
            'active': 0,  
            'start' : ['2007-05-07', '2010-04-26'],  
            'stop'  : ['2008-02-13', '2010-11-15']  
        }  
    '''  
    '''
    if 'orders' not in c:  
        c.orders = {}               # Move these to initialize() for better efficiency.  
        c.dates  = { 
            'active': 0,  
            'start' : [],           # Start dates, option  
            'stop'  : []            # Stop  dates, option  
        }  
    '''

    # If the dates 'start' or 'stop' lists have something in them, sets them.  
    if c.dates['start'] or c.dates['stop']:  
        date = str(get_datetime().date())
        if   date in c.dates['start']:    # See if there's a match to start  
            c.dates['active'] = 1  
        elif date in c.dates['stop']:     #   ... or to stop  
            c.dates['active'] = 0  
    else:  
        c.dates['active'] = 1  # Set to active b/c no conditions

    if c.dates['active'] == 0:  
        return                 # Skip if off

    def _minute():   # To preface each line with the minute of the day.  
        if get_environment('data_frequency') == 'minute':  
            bar_dt = get_datetime().astimezone(timezone('US/Eastern'))  
            minute = (bar_dt.hour * 60) + bar_dt.minute - 570  # (-570 = 9:31a)  
            return str(minute).rjust(3)  
        return ''    # Daily mode, just leave it out.

    def _orders(to_log):    # So all logging comes from the same line number,  
        log.info(to_log)    #   for vertical alignment in the logging window.

    ordrs = c.orders.copy()    # Independent copy to allow deletes  
    for id in ordrs:  
        o    = get_order(id)  
        sec  = o.sid ; sym = sec.symbol  
        oid  = o.id if log_ids else ''  
        cash = 'cash {}'.format(int(c.portfolio.cash)) if log_cash else ''  
        prc  = '%.2f' % data.current(sec, 'price')
        if o.filled:        # Filled at least some  
            trade  = 'Bot' if o.amount > 0 else 'Sold'  
            filled = '{}'.format(o.amount)  
            if o.filled == o.amount:    # complete  
                if 0 < c.orders[o.id] < o.amount:  
                    filled  = 'all/{}'.format(o.amount)  
                del c.orders[o.id]  
            else:  
                done_prv       = c.orders[o.id]       # previously filled ttl  
                filled_this    = o.filled - done_prv  # filled this time, can be 0  
                c.orders[o.id] = o.filled             # save for increments math  
                filled         = '{}/{}'.format(filled_this, o.amount)  
            _orders(' {}      {} {} {} at {}   {} {}'.format(_minute(),  
                trade, filled, sym, prc, cash, oid))  
        else:  
            canceled = 'canceled' if o.status == 2 else ''  
            _orders(' {}         {} {} unfilled {} {}'.format(_minute(),  
                    o.sid.symbol, o.amount, canceled, oid))  
            if canceled: del c.orders[o.id]

    for oo_list in get_open_orders().values(): # Open orders list  
        for o in oo_list:  
            sec  = o.sid ; sym = sec.symbol  
            oid  = o.id if log_ids else ''  
            cash = 'cash {}'.format(int(c.portfolio.cash)) if log_cash else ''  
            prc  = '%.2f' % data.current(sec, 'price')
            if o.status == 2:                  # Canceled  
                _orders(' {}    Canceled {} {} order   {} {}'.format(_minute(),  
                        trade, o.amount, sym, prc, cash, oid))  
                del c.orders[o.id]  
            elif o.id not in c.orders:         # New  
                c.orders[o.id] = 0  
                trade = 'Buy' if o.amount > 0 else 'Sell'  
                if o.limit:                    # Limit order  
                    _orders(' {}   {} {} {} now {} limit {}   {} {}'.format(_minute(),  
                        trade, o.amount, sym, prc, o.limit, cash, oid))  
                elif o.stop:                   # Stop order  
                    _orders(' {}   {} {} {} now {} stop {}   {} {}'.format(_minute(),  
                        trade, o.amount, sym, prc, o.stop, cash, oid))  
                else:                          # Market order  
                    _orders(' {}   {} {} {} at {}   {} {}'.format(_minute(),  
                        trade, o.amount, sym, prc, cash, oid))  
                
def pvr(context, data):  
    ''' Custom chart and/or log of profit_vs_risk returns and related information  
    '''  
    # # # # # # # # # #  Options  # # # # # # # # # #  
    record_max_lvrg = 1         # Maximum leverage encountered  
    record_leverage = 0         # Leverage (context.account.leverage)  
    record_q_return = 0         # Quantopian returns (percentage)  
    record_pvr      = 1         # Profit vs Risk returns (percentage)  
    record_pnl      = 1         # Profit-n-Loss  
    record_shorting = 0         # Total value of any shorts  
    record_overshrt = 0         # Shorts beyond longs+cash  
    record_risk     = 0         # Risked, max cash spent or shorts beyond longs+cash  
    record_risk_hi  = 1         # Highest risk overall  
    record_cash     = 0         # Cash available  
    record_cash_low = 1         # Any new lowest cash level  
    logging         = 1         # Also to logging window conditionally (1) or not (0)  
    log_method      = 'risk_hi' # 'daily' or 'risk_hi'

    from pytz import timezone   # Python will only do once, makes this portable.  
                                #   Move to top of algo for better efficiency.  
    c = context  # Brevity is the soul of wit -- Shakespeare [for efficiency, readability]  
    if 'pvr' not in c:  
        date_strt = get_environment('start').date()  
        date_end  = get_environment('end').date()  
        cash_low  = c.portfolio.starting_cash  
        mode      = get_environment('data_frequency')  
        c.pvr = {  
            'max_lvrg': 0,  
            'risk_hi' : 0,  
            'days'    : 0.0,  
            'date_prv': '',  
            'cash_low': cash_low,  
            'date_end': date_end,  
            'mode'    : mode,  
            'run_str' : '{} to {}  {}  {}'.format(date_strt,date_end,int(cash_low),mode)  
        }  
        log.info(c.pvr['run_str'])  
    pvr_rtrn     = 0            # Profit vs Risk returns based on maximum spent  
    profit_loss  = 0            # Profit-n-loss  
    shorts       = 0            # Shorts value  
    longs        = 0            # Longs  value  
    overshorts   = 0            # Shorts value beyond longs plus cash  
    new_risk_hi  = 0  
    new_cash_low = 0                           # To trigger logging in cash_low case  
    lvrg         = c.account.leverage          # Standard leverage, in-house  
    date         = get_datetime().date()       # To trigger logging in daily case  
    cash         = c.portfolio.cash  
    start        = c.portfolio.starting_cash  
    cash_dip     = int(max(0, start - cash))  
    q_rtrn       = 100 * (c.portfolio.portfolio_value - start) / start

    if int(cash) < c.pvr['cash_low']:                # New cash low  
        new_cash_low = 1  
        c.pvr['cash_low']   = int(cash)  
        if record_cash_low:  
            record(CashLow = int(c.pvr['cash_low'])) # Lowest cash level hit

    if record_max_lvrg:  
        if c.account.leverage > c.pvr['max_lvrg']:  
            c.pvr['max_lvrg'] = c.account.leverage  
            record(MaxLv = c.pvr['max_lvrg'])        # Maximum leverage

    if record_pnl:  
        profit_loss = c.portfolio.pnl  
        record(PnL = profit_loss)                    # "Profit and Loss" in dollars

    for p in c.portfolio.positions:
        if data.can_trade(p):
            shrs = c.portfolio.positions[p].amount 
            if shrs < 0:  
                shorts += int(abs(shrs * data.current(p, 'price')))
            if shrs > 0:  
                longs  += int(shrs * data.current(p, 'price'))

    if shorts > longs + cash: overshorts = shorts             # Shorts when too high  
    if record_shorting: record(Shorts  = shorts)              # Shorts value as a positve  
    if record_overshrt: record(OvrShrt = overshorts)          # Shorts value as a positve  
    if record_cash:     record(Cash = int(c.portfolio.cash))  # Cash  
    if record_leverage: record(Lvrg = c.account.leverage)     # Leverage

    risk = int(max(cash_dip, shorts))  
    if record_risk: record(Risk = risk)       # Amount in play, maximum of shorts or cash used

    if risk > c.pvr['risk_hi']:  
        c.pvr['risk_hi'] = risk  
        new_risk_hi = 1

        if record_risk_hi:  
            record(RiskHi = c.pvr['risk_hi']) # Highest risk overall

    if record_pvr:      # Profit_vs_Risk returns based on max amount actually spent (risk high)  
        if c.pvr['risk_hi'] != 0:     # Avoid zero-divide  
            pvr_rtrn = 100 * (c.portfolio.portfolio_value - start) / c.pvr['risk_hi']  
            record(PvR = pvr_rtrn)            # Profit_vs_Risk returns

    if record_q_return:  
        record(QRet = q_rtrn)                 # Quantopian returns to compare to pvr returns curve

    def _minute():   # To preface each line with minute of the day.  
        if get_environment('data_frequency') == 'minute':  
            bar_dt = get_datetime().astimezone(timezone('US/Eastern'))  
            minute = (bar_dt.hour * 60) + bar_dt.minute - 570  # (-570 = 9:31a)  
            return str(minute).rjust(3)  
        return ''    # Daily mode, just leave it out.

    def _pvr_():  
            log.info('PvR {} %/day     {}'.format(  
                '%.4f' % (pvr_rtrn / c.pvr['days']), c.pvr['run_str']))  
            log.info('  Profited {} on {} activated/transacted for PvR of {}%'.format(  
                '%.0f' % (c.portfolio.portfolio_value - start), '%.0f' % c.pvr['risk_hi'],  
                '%.1f' % pvr_rtrn))  
            log.info('  QRet {} PvR {} CshLw {} MxLv {} RskHi {} Shrts {}'.format(  
                '%.2f' % q_rtrn, '%.2f' % pvr_rtrn, '%.0f' % c.pvr['cash_low'],  
                '%.2f' % c.pvr['max_lvrg'], '%.0f' % c.pvr['risk_hi'], '%.0f' % shorts))

    if logging:  
        if log_method == 'risk_hi' and new_risk_hi \
          or log_method == 'daily' and c.pvr['date_prv'] != date \
          or new_cash_low:  
            qret    = ' QRet '   + '%.1f' % q_rtrn  
            lv      = ' Lv '     + '%.1f' % lvrg              if record_leverage else ''  
            pvr     = ' PvR '    + '%.1f' % pvr_rtrn          if record_pvr      else ''  
            pnl     = ' PnL '    + '%.0f' % profit_loss       if record_pnl      else ''  
            csh     = ' Cash '   + '%.0f' % cash              if record_cash     else ''  
            shrt    = ' Shrt '   + '%.0f' % shorts            if record_shorting else ''  
            ovrshrt = ' Shrt '   + '%.0f' % overshorts        if record_overshrt else ''  
            risk    = ' Risk '   + '%.0f' % risk              if record_risk     else ''  
            mxlv    = ' MaxLv '  + '%.2f' % c.pvr['max_lvrg'] if record_max_lvrg else ''  
            csh_lw  = ' CshLw '  + '%.0f' % c.pvr['cash_low'] if record_cash_low else ''  
            rsk_hi  = ' RskHi '  + '%.0f' % c.pvr['risk_hi']  if record_risk_hi  else ''  
            log.info('{}{}{}{}{}{}{}{}{}{}{}{}'.format(_minute(),  
               lv, mxlv, qret, pvr, pnl, csh, csh_lw, shrt, ovrshrt, risk, rsk_hi))  
    if c.pvr['date_prv'] != date: c.pvr['days'] += 1.0  
    if c.pvr['days'] % 130 == 0 and _minute() == '100': _pvr_()  
    c.pvr['date_prv'] = date  
    if c.pvr['date_end'] == date:  
        # Summary on last minute of last day.  
        # If using schedule_function(), backtest last day/time may need to match for this to execute.  
        if 'pvr_summary_done' not in c: c.pvr_summary_done = 0  
        log_summary = 0  
        if c.pvr['mode'] == 'daily' and get_datetime().date() == c.pvr['date_end']:  
            log_summary = 1  
        elif c.pvr['mode'] == 'minute' and get_datetime() == get_environment('end'):  
            log_summary = 1  
        if log_summary and not c.pvr_summary_done:  
            _pvr_()  
            c.pvr_summary_done = 1
def minut():   # To preface each line with the minute of the day.
               # Added to be used in trade()
    if get_environment('data_frequency') == 'minute':
        bar_dt = get_datetime().astimezone(timezone('US/Eastern'))
        minute = (bar_dt.hour * 60) + bar_dt.minute - 570  # (-570 = 9:31a)
        return str(minute).rjust(3)
    return ''    # Daily mode, just leave it out.

There was a runtime error.

Dmitry,

That does look like a pricing bug to me. I've filed the issue internally.

Here's your version of the algorithm (which is different from mine as yours only goes long, includes PvR, diff capital base) without HBI included.

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 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 pytz import timezone       # Python only does once, makes this portable.  
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, AverageDollarVolume

# Premium version avilable at
# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.estimize import (
    ConsensusEstimizeEPS,
    ConsensusWallstreetEPS,
    ConsensusEstimizeRevenue, 
    ConsensusWallstreetRevenue
)

# 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
)

from quantopian.pipeline.data.accern import alphaone as alphaone
# Premium version availabe at
# https://www.quantopian.com/data/accern/alphaone
# from quantopian.pipeline.data.accern import alphaone as alphaone

class PercentSurprise(CustomFactor):
    window_length = 1
    inputs = [ConsensusEstimizeEPS.previous_actual_value,
              ConsensusEstimizeEPS.previous_mean]

    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()  

    # Instantiating our factors  
    factor = PercentSurprise()

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Filter down to stocks in the top/bottom  
    longs = (factor >= context.min_surprise) & (factor <= context.max_surprise)

    # Set our pipeline screens  
    # Filter down stocks using sentiment  
    article_sentiment = alphaone.article_sentiment.latest
    top_universe = is_liquid & longs & article_sentiment.notnan() & (article_sentiment > .45)

    # Add longs to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(BusinessDaysSincePreviousEarnings(), 'pe')
    
    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))
    
    context.queue_list = []
    context.track_orders = 1    # toggle on|off
    
    context.orders = {}               # Move these to initialize() for better efficiency.  
    context.dates  = { 
            'active': 0,  
            'start' : [],           # Start dates, option  
            'stop'  : []            # Stop  dates, option  
        }  
    
    #: Set to Long Only
    set_long_only()

    #: Declaring the days to hold, change this to what you want)))
    context.days_to_hold = 5
    #: 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 = .06

    # 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_close())
    # Order our positions
    schedule_function(func=queues,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())
    '''
    # For every minute available (max is 6 hours and 30 minutes)
    total_minutes = 6*60 + 30

    for i in range(total_minutes):
    # Every 30 minutes run schedule
        if i % 30 == 0:
            schedule_function(check_positions_for_loss_or_profit,
                              date_rules.every_day(),
                              time_rules.market_open(minutes=i),
                              True)
    '''

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]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]


def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        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 trade(context, data):    # Process any queued orders
    if get_open_orders(): return    # Wait for fills

    c = context
    mult = .95        # Multiplier for weights in orders, cash vs
                       #  slippage, commissions, to avoid negative cash.
    log_changes = 1    # Whether to log weight|allocation changes.
    sells = 0          # Indicator, whether any sells happened.
    qlist = sorted(c.queue_list)[:]  # Make an independent copy to allow remove().

    for o in qlist:    # Each order queued, process any sells
        security  = o[1] ; weight = o[0]
        pf_value_now = c.portfolio.positions[security].amount * data.current(security, 'price')
        pf_ratio_now = pf_value_now / c.portfolio.portfolio_value
        if weight < pf_ratio_now:   # sell, is decrease in allocation
            if log_changes:
                log.info('   {} {} {} ==> {}'.format(
                    minut(), security.symbol, '%.3f' % pf_ratio_now, '%.3f' % weight))
            order_target_percent(security, mult * weight)    # Selling
            c.queue_list.remove(o)
            sells = 1  # let these settle before/if any buys

    if sells: return   # let any sells go thru before buys

    '''
    To do if Robinhood: Make sure T+3 is satisfied before buys here.
    Untested ...
    c = context
    date = get_datetime().date()
    if c.date_prv != date:
        c.day_count += 1
        c.date_prv = date
    if c.day_count <= 3:
        return
    else:
        c.day_count = 0
    '''

    for o in qlist:    # Should be all buys at this point
        security  = o[1] ; weight = o[0]
        pf_value_now = c.portfolio.positions[security].amount * data.current(security, 'price')
        pf_ratio_now = pf_value_now / c.portfolio.portfolio_value
        if weight > pf_ratio_now:   # buy, is increase in allocation
            if log_changes:
                log.info('   {} {} {} ==> {}'.format(
                    minut(), security.symbol, '%.3f' % pf_ratio_now, '%.3f' % weight))
            order_target_percent(security, mult * weight)    # Buying
            c.queue_list.remove(o)
        
 
def queues(context, data): # Was Order_Positions Before
    """
    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.
    """
    if context.queue_list: return    # wait for orders to clear

    port = context.portfolio.positions
    #record(leverage=context.account.leverage)
    
    # Check our positions for loss or profit and exit if necessary
    check_positions_for_loss_or_profit(context, data)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    sell_allocation = 0
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1  
                if context.stocks_held[security] >= context.days_to_hold:  
                    context.queue_list.append((sell_allocation, security))   
                    del context.stocks_held[security]
            # If we've deleted it but it still hasn't been exited. Try exiting again  
          #  else:  
          #      log.info("Haven't yet exited %s, ordering again" % security.symbol)  
          #      order_target_percent(security, 0)  

    # Check our current positions
    current_positive_pos = [pos for pos in port if (port[pos].amount > 0 and pos in context.stocks_held)]
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos
    
    # Rebalance our positive surprise securities (existing + new)                
    for security in positive_stocks:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade and security != sid(32497):
            buy_allocation = 0.95 / len(positive_stocks)
            context.queue_list.append((buy_allocation, security))
            if context.stocks_held.get(security) is None:
                context.stocks_held[security] = 0
    

def check_positions_for_loss_or_profit(context, data):
    # Sell our positions on longs for profit or loss
    if context.queue_list: return    # wait for orders to clear
    sell_allocation = 0
    for security in context.portfolio.positions:
        is_stock_held = context.stocks_held.get(security) > 0
        if data.can_trade(security) and is_stock_held and not get_open_orders(security):
            current_position = context.portfolio.positions[security].amount  
            cost_basis = context.portfolio.positions[security].cost_basis  
            price = data.current(security, 'price')
            # On Long & Profit
            if price >= cost_basis * 1.04 and current_position > 0:  
                context.queue_list.append((sell_allocation, security))  
                log.info( str(security) + ' Sold Long for Profit')  
                del context.stocks_held[security]  
            # On Long & Loss
            if price <= cost_basis * 0.95 and current_position > 0:  
                context.queue_list.append((sell_allocation, security))
                log.info( str(security) + ' Sold Long for Loss')  
                del context.stocks_held[security]
    track_orders(context, data)

def handle_data(context, data):
    if get_open_orders():
        track_orders(context, data)    # for filled orders
        return
    if context.queue_list:
        trade(context, data)
    track_orders(context, data)        # for new orders and last-frame-filled's
    #check_positions_for_loss_or_profit(context, data)
    pvr(context, data)    
    
def track_orders(context, data):  # Log orders created, filled, unfilled or canceled.  
    '''      https://www.quantopian.com/posts/track-orders  
    Status:  
       0 - Unfilled  
       1 - Filled (can be partial)  
       2 - Canceled  
    '''  
    c = context  
    log_cash = 1    # Show cash values in logging window or not.  
    log_ids  = 1    # Include order id's in logging window or not.

    ''' Start and stop date options ...  
    To not overwhelm the logging window, start/stop dates can be entered  
      either below or in initialize() if you move to there for better efficiency.  
    Example:  
        c.dates  = {  
            'active': 0,  
            'start' : ['2007-05-07', '2010-04-26'],  
            'stop'  : ['2008-02-13', '2010-11-15']  
        }  
    '''  
    '''
    if 'orders' not in c:  
        c.orders = {}               # Move these to initialize() for better efficiency.  
        c.dates  = { 
            'active': 0,  
            'start' : [],           # Start dates, option  
            'stop'  : []            # Stop  dates, option  
        }  
    '''

    # If the dates 'start' or 'stop' lists have something in them, sets them.  
    if c.dates['start'] or c.dates['stop']:  
        date = str(get_datetime().date())
        if   date in c.dates['start']:    # See if there's a match to start  
            c.dates['active'] = 1  
        elif date in c.dates['stop']:     #   ... or to stop  
            c.dates['active'] = 0  
    else:  
        c.dates['active'] = 1  # Set to active b/c no conditions

    if c.dates['active'] == 0:  
        return                 # Skip if off

    def _minute():   # To preface each line with the minute of the day.  
        if get_environment('data_frequency') == 'minute':  
            bar_dt = get_datetime().astimezone(timezone('US/Eastern'))  
            minute = (bar_dt.hour * 60) + bar_dt.minute - 570  # (-570 = 9:31a)  
            return str(minute).rjust(3)  
        return ''    # Daily mode, just leave it out.

    def _orders(to_log):    # So all logging comes from the same line number,  
        log.info(to_log)    #   for vertical alignment in the logging window.

    ordrs = c.orders.copy()    # Independent copy to allow deletes  
    for id in ordrs:  
        o    = get_order(id)  
        sec  = o.sid ; sym = sec.symbol  
        oid  = o.id if log_ids else ''  
        cash = 'cash {}'.format(int(c.portfolio.cash)) if log_cash else ''  
        prc  = '%.2f' % data.current(sec, 'price')
        if o.filled:        # Filled at least some  
            trade  = 'Bot' if o.amount > 0 else 'Sold'  
            filled = '{}'.format(o.amount)  
            if o.filled == o.amount:    # complete  
                if 0 < c.orders[o.id] < o.amount:  
                    filled  = 'all/{}'.format(o.amount)  
                del c.orders[o.id]  
            else:  
                done_prv       = c.orders[o.id]       # previously filled ttl  
                filled_this    = o.filled - done_prv  # filled this time, can be 0  
                c.orders[o.id] = o.filled             # save for increments math  
                filled         = '{}/{}'.format(filled_this, o.amount)  
            _orders(' {}      {} {} {} at {}   {} {}'.format(_minute(),  
                trade, filled, sym, prc, cash, oid))  
        else:  
            canceled = 'canceled' if o.status == 2 else ''  
            _orders(' {}         {} {} unfilled {} {}'.format(_minute(),  
                    o.sid.symbol, o.amount, canceled, oid))  
            if canceled: del c.orders[o.id]

    for oo_list in get_open_orders().values(): # Open orders list  
        for o in oo_list:  
            sec  = o.sid ; sym = sec.symbol  
            oid  = o.id if log_ids else ''  
            cash = 'cash {}'.format(int(c.portfolio.cash)) if log_cash else ''  
            prc  = '%.2f' % data.current(sec, 'price')
            if o.status == 2:                  # Canceled  
                _orders(' {}    Canceled {} {} order   {} {}'.format(_minute(),  
                        trade, o.amount, sym, prc, cash, oid))  
                del c.orders[o.id]  
            elif o.id not in c.orders:         # New  
                c.orders[o.id] = 0  
                trade = 'Buy' if o.amount > 0 else 'Sell'  
                if o.limit:                    # Limit order  
                    _orders(' {}   {} {} {} now {} limit {}   {} {}'.format(_minute(),  
                        trade, o.amount, sym, prc, o.limit, cash, oid))  
                elif o.stop:                   # Stop order  
                    _orders(' {}   {} {} {} now {} stop {}   {} {}'.format(_minute(),  
                        trade, o.amount, sym, prc, o.stop, cash, oid))  
                else:                          # Market order  
                    _orders(' {}   {} {} {} at {}   {} {}'.format(_minute(),  
                        trade, o.amount, sym, prc, cash, oid))  
                
def pvr(context, data):  
    ''' Custom chart and/or log of profit_vs_risk returns and related information  
    '''  
    # # # # # # # # # #  Options  # # # # # # # # # #  
    record_max_lvrg = 1         # Maximum leverage encountered  
    record_leverage = 0         # Leverage (context.account.leverage)  
    record_q_return = 0         # Quantopian returns (percentage)  
    record_pvr      = 1         # Profit vs Risk returns (percentage)  
    record_pnl      = 1         # Profit-n-Loss  
    record_shorting = 0         # Total value of any shorts  
    record_overshrt = 0         # Shorts beyond longs+cash  
    record_risk     = 0         # Risked, max cash spent or shorts beyond longs+cash  
    record_risk_hi  = 1         # Highest risk overall  
    record_cash     = 0         # Cash available  
    record_cash_low = 1         # Any new lowest cash level  
    logging         = 1         # Also to logging window conditionally (1) or not (0)  
    log_method      = 'risk_hi' # 'daily' or 'risk_hi'

    from pytz import timezone   # Python will only do once, makes this portable.  
                                #   Move to top of algo for better efficiency.  
    c = context  # Brevity is the soul of wit -- Shakespeare [for efficiency, readability]  
    if 'pvr' not in c:  
        date_strt = get_environment('start').date()  
        date_end  = get_environment('end').date()  
        cash_low  = c.portfolio.starting_cash  
        mode      = get_environment('data_frequency')  
        c.pvr = {  
            'max_lvrg': 0,  
            'risk_hi' : 0,  
            'days'    : 0.0,  
            'date_prv': '',  
            'cash_low': cash_low,  
            'date_end': date_end,  
            'mode'    : mode,  
            'run_str' : '{} to {}  {}  {}'.format(date_strt,date_end,int(cash_low),mode)  
        }  
        log.info(c.pvr['run_str'])  
    pvr_rtrn     = 0            # Profit vs Risk returns based on maximum spent  
    profit_loss  = 0            # Profit-n-loss  
    shorts       = 0            # Shorts value  
    longs        = 0            # Longs  value  
    overshorts   = 0            # Shorts value beyond longs plus cash  
    new_risk_hi  = 0  
    new_cash_low = 0                           # To trigger logging in cash_low case  
    lvrg         = c.account.leverage          # Standard leverage, in-house  
    date         = get_datetime().date()       # To trigger logging in daily case  
    cash         = c.portfolio.cash  
    start        = c.portfolio.starting_cash  
    cash_dip     = int(max(0, start - cash))  
    q_rtrn       = 100 * (c.portfolio.portfolio_value - start) / start

    if int(cash) < c.pvr['cash_low']:                # New cash low  
        new_cash_low = 1  
        c.pvr['cash_low']   = int(cash)  
        if record_cash_low:  
            record(CashLow = int(c.pvr['cash_low'])) # Lowest cash level hit

    if record_max_lvrg:  
        if c.account.leverage > c.pvr['max_lvrg']:  
            c.pvr['max_lvrg'] = c.account.leverage  
            record(MaxLv = c.pvr['max_lvrg'])        # Maximum leverage

    if record_pnl:  
        profit_loss = c.portfolio.pnl  
        record(PnL = profit_loss)                    # "Profit and Loss" in dollars

    for p in c.portfolio.positions:
        if data.can_trade(p):
            shrs = c.portfolio.positions[p].amount 
            if shrs < 0:  
                shorts += int(abs(shrs * data.current(p, 'price')))
            if shrs > 0:  
                longs  += int(shrs * data.current(p, 'price'))

    if shorts > longs + cash: overshorts = shorts             # Shorts when too high  
    if record_shorting: record(Shorts  = shorts)              # Shorts value as a positve  
    if record_overshrt: record(OvrShrt = overshorts)          # Shorts value as a positve  
    if record_cash:     record(Cash = int(c.portfolio.cash))  # Cash  
    if record_leverage: record(Lvrg = c.account.leverage)     # Leverage

    risk = int(max(cash_dip, shorts))  
    if record_risk: record(Risk = risk)       # Amount in play, maximum of shorts or cash used

    if risk > c.pvr['risk_hi']:  
        c.pvr['risk_hi'] = risk  
        new_risk_hi = 1

        if record_risk_hi:  
            record(RiskHi = c.pvr['risk_hi']) # Highest risk overall

    if record_pvr:      # Profit_vs_Risk returns based on max amount actually spent (risk high)  
        if c.pvr['risk_hi'] != 0:     # Avoid zero-divide  
            pvr_rtrn = 100 * (c.portfolio.portfolio_value - start) / c.pvr['risk_hi']  
            record(PvR = pvr_rtrn)            # Profit_vs_Risk returns

    if record_q_return:  
        record(QRet = q_rtrn)                 # Quantopian returns to compare to pvr returns curve

    def _minute():   # To preface each line with minute of the day.  
        if get_environment('data_frequency') == 'minute':  
            bar_dt = get_datetime().astimezone(timezone('US/Eastern'))  
            minute = (bar_dt.hour * 60) + bar_dt.minute - 570  # (-570 = 9:31a)  
            return str(minute).rjust(3)  
        return ''    # Daily mode, just leave it out.

    def _pvr_():  
            log.info('PvR {} %/day     {}'.format(  
                '%.4f' % (pvr_rtrn / c.pvr['days']), c.pvr['run_str']))  
            log.info('  Profited {} on {} activated/transacted for PvR of {}%'.format(  
                '%.0f' % (c.portfolio.portfolio_value - start), '%.0f' % c.pvr['risk_hi'],  
                '%.1f' % pvr_rtrn))  
            log.info('  QRet {} PvR {} CshLw {} MxLv {} RskHi {} Shrts {}'.format(  
                '%.2f' % q_rtrn, '%.2f' % pvr_rtrn, '%.0f' % c.pvr['cash_low'],  
                '%.2f' % c.pvr['max_lvrg'], '%.0f' % c.pvr['risk_hi'], '%.0f' % shorts))

    if logging:  
        if log_method == 'risk_hi' and new_risk_hi \
          or log_method == 'daily' and c.pvr['date_prv'] != date \
          or new_cash_low:  
            qret    = ' QRet '   + '%.1f' % q_rtrn  
            lv      = ' Lv '     + '%.1f' % lvrg              if record_leverage else ''  
            pvr     = ' PvR '    + '%.1f' % pvr_rtrn          if record_pvr      else ''  
            pnl     = ' PnL '    + '%.0f' % profit_loss       if record_pnl      else ''  
            csh     = ' Cash '   + '%.0f' % cash              if record_cash     else ''  
            shrt    = ' Shrt '   + '%.0f' % shorts            if record_shorting else ''  
            ovrshrt = ' Shrt '   + '%.0f' % overshorts        if record_overshrt else ''  
            risk    = ' Risk '   + '%.0f' % risk              if record_risk     else ''  
            mxlv    = ' MaxLv '  + '%.2f' % c.pvr['max_lvrg'] if record_max_lvrg else ''  
            csh_lw  = ' CshLw '  + '%.0f' % c.pvr['cash_low'] if record_cash_low else ''  
            rsk_hi  = ' RskHi '  + '%.0f' % c.pvr['risk_hi']  if record_risk_hi  else ''  
            log.info('{}{}{}{}{}{}{}{}{}{}{}{}'.format(_minute(),  
               lv, mxlv, qret, pvr, pnl, csh, csh_lw, shrt, ovrshrt, risk, rsk_hi))  
    if c.pvr['date_prv'] != date: c.pvr['days'] += 1.0  
    if c.pvr['days'] % 130 == 0 and _minute() == '100': _pvr_()  
    c.pvr['date_prv'] = date  
    if c.pvr['date_end'] == date:  
        # Summary on last minute of last day.  
        # If using schedule_function(), backtest last day/time may need to match for this to execute.  
        if 'pvr_summary_done' not in c: c.pvr_summary_done = 0  
        log_summary = 0  
        if c.pvr['mode'] == 'daily' and get_datetime().date() == c.pvr['date_end']:  
            log_summary = 1  
        elif c.pvr['mode'] == 'minute' and get_datetime() == get_environment('end'):  
            log_summary = 1  
        if log_summary and not c.pvr_summary_done:  
            _pvr_()  
            c.pvr_summary_done = 1
def minut():   # To preface each line with the minute of the day.
               # Added to be used in trade()
    if get_environment('data_frequency') == 'minute':
        bar_dt = get_datetime().astimezone(timezone('US/Eastern'))
        minute = (bar_dt.hour * 60) + bar_dt.minute - 570  # (-570 = 9:31a)
        return str(minute).rjust(3)
    return ''    # Daily mode, just leave it out.

There was a runtime error.

@Seong - thanks for the update! However, is there a way to keep it long/short without jeopardizing liquidity? Or is that due to the bug?

Daniel, we already have liquidity filters in place. Please adjust that to what you're looking for.

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7  

I meant to reference @Dmitry in my reply this morning about HBI's pricing issue

@SEong - It's a very interesting strategy, congratulations!

But I think it has a disadvantage:
In the case that just a single company fits the requirements, it will set 100% of the capital in this company.
This increases the volatility and exposure.

Maybe, you can set a minimun percent for each asset:

order_target_percent(security, min(1.0 / len(positive_stocks), 0.5))

And max() for short:
order_target_percent(security, **max**(-1.0 / len(negative_stocks), -0.5))

Thanks,

I cloned this(*) version and the max/min change reduces the drawdown (0.13 to 0.9) and the volatility (0.19 to 0.14) also the returns.

(*) https://www.quantopian.com/posts/long-slash-short-earnings-sentiment-trading-strategy#572028837d121053c8000866

Clone Algorithm
32
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.data.builtin import USEquityPricing
from quantopian.pipeline.factors import CustomFactor, AverageDollarVolume

# Premium version avilable at
# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.estimize import (
    ConsensusEstimizeEPS,
    ConsensusWallstreetEPS,
    ConsensusEstimizeRevenue, 
    ConsensusWallstreetRevenue
)

# 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
)

from quantopian.pipeline.data.accern import alphaone_free as alphaone
# Premium version availabe at
# https://www.quantopian.com/data/accern/alphaone
# from quantopian.pipeline.data.accern import alphaone as alphaone

class PercentSurprise(CustomFactor):
    window_length = 1
    inputs = [ConsensusEstimizeEPS.previous_actual_value,
              ConsensusEstimizeEPS.previous_mean]

    def compute(self, today, assets, out, actual_eps, estimize_eps):
        out[:] = (actual_eps[-1] - estimize_eps[-1])/(estimize_eps[-1] + 0)
        
class DaysSinceRelease(CustomFactor):
    # Only getting the previous quarter's estimize surprise
    window_length = 1
    inputs = [EarningsCalendar.previous_announcement,
              ConsensusEstimizeEPS.previous_release_date]
    
    def compute(self, today, assets, out,
                earnings_announcement, estimize_release):
        days = estimize_release - earnings_announcement
        out[:] = abs(days.astype('timedelta64[D]').astype(int))

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

    # Instantiating our factors  
    factor = PercentSurprise()
    
    # Get the days since estimize release date and the latest
    # Earnings release date
    days_since_release = DaysSinceRelease()
    previous_estimize_release_date = \
        ConsensusEstimizeEPS.previous_release_date.latest

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Filter down to stocks in the top/bottom according to
    # the earnings surprise
    longs = (factor >= context.min_surprise) & (factor <= context.max_surprise)
    shorts = (factor <= -context.min_surprise) & (factor >= -context.max_surprise)

    # Set our pipeline screens  
    # Filter down stocks using sentiment  
    article_sentiment = alphaone.article_sentiment.latest
    top_universe = is_liquid & longs & article_sentiment.notnan() \
        & (article_sentiment > .45)
    bottom_universe = is_liquid & shorts & article_sentiment.notnan() \
        & (article_sentiment < -.45)

    # Add long/shorts to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(bottom_universe, "shorts")
    pipe.add(BusinessDaysSincePreviousEarnings(), 'pe')
    pipe.set_screen((days_since_release <= 7) &
                    (previous_estimize_release_date.notnull()))
    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 = 4
    #: 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 = .06

    #: 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_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())

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]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]
    context.negative_surprise = assets_in_universe[results.shorts]

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        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
    record(leverage=context.account.leverage)

    # Check our positions for loss or profit and exit if necessary
    check_positions_for_loss_or_profit(context, data)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1  
                if context.stocks_held[security] >= context.days_to_hold:  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]  
            # If we've deleted it but it still hasn't been exited. Try exiting again  
            else:  
                log.info("Haven't yet exited %s, ordering again" % security.symbol)  
                order_target_percent(security, 0)  

    # Check our current 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.tolist() + current_negative_pos
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos
    
    # Rebalance our negative surprise securities (existing + new)
    for security in negative_stocks:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            order_target_percent(security, max(-1.0 / len(negative_stocks), -0.5))
            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:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            order_target_percent(security, min(1.0 / len(positive_stocks), 0.5))
            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.current(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 check_positions_for_loss_or_profit(context, data):
    # Sell our positions on longs/shorts for profit or loss
    for security in context.portfolio.positions:
        is_stock_held = context.stocks_held.get(security) >= 0
        if data.can_trade(security) and is_stock_held and not get_open_orders(security):
            current_position = context.portfolio.positions[security].amount  
            cost_basis = context.portfolio.positions[security].cost_basis  
            price = data.current(security, 'price')
            # On Long & Profit
            if price >= cost_basis * 1.06 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Profit')  
                del context.stocks_held[security]  
            # On Short & Profit
            if price <= cost_basis* 0.94 and current_position < 0:
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Profit')  
                del context.stocks_held[security]
            # On Long & Loss
            if price <= cost_basis * 0.94 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Loss')  
                del context.stocks_held[security]  
            # On Short & Loss
            if price >= cost_basis * 1.06 and current_position < 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Loss')  
                del context.stocks_held[security]  
There was a runtime error.

Edit: Thanks for posting Martin, my previous reply was out of place. This is a great algorithm!

Seong,

My previous question is a general question... In the source code above I am wondering what limits order execution. I know you can use the schedule function to limit the number of times it trades... however this doesn't seem to work very effectively.

What I am trying to communicate is how do you tell the computer to enter a position and hold it rather than add to the position if the criteria is met?

In the above example I see "and current_position > 0" for a long profit and "current_position < 0" for a short profit.... does this simple code tell the computer to take a position and hold it until the 3% profit is achieved? Or is there a different code in this algorithm that tells the computer to do this?

What is the code in the above algorithm that says... ? "Take a position and hold it until 3% profit is achieved." The holding part is what I am confused about, I want to understand this.

Best,
Lovis

Lovis,

We enter the positions in order_positions. These positions are held until either a 3% profit or loss is achieved (check_positions_for_loss_or_profit) OR 4 days have passed since we've entered that stock (order_positions).

It seems like you are asking a general python and Quantopian question. Going through our tutorial (https://www.quantopian.com/tutorials/getting-started) will most certainly answer most of the questions you have about how Quantopian algorithms operate.

Thanks!
Seong

My first post here.

I am not sure if I should post because I did nothing except tweaked the take profit and stop loss percent, as well as the weightage. Take profit at 7%, stop loss 4%. Weightage for long at 98%, weightage for short at 25%. A long only strategy (just comment out the line to short) would produce even better return at 108% over the same period, but it'd have periods where it dips into the red region (i.e. dropping below starting capital) during November and December 2012. High weightage on shorts seems to produce poorer result during bull market. But without shorts the performance tanks when there is correction.

I'd say with more tweaking, and addition of other parameters the performance could be pushed further. I like high Sortino ratio for consistency, as well as low 6% max drawdown.

Thanks for providing a framework for newcomers to learn.

Clone Algorithm
118
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.data.builtin import USEquityPricing
from quantopian.pipeline.factors import CustomFactor, AverageDollarVolume

# Premium version avilable at
# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.estimize import (
    ConsensusEstimizeEPS,
    ConsensusWallstreetEPS,
    ConsensusEstimizeRevenue, 
    ConsensusWallstreetRevenue
)

# 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
)

from quantopian.pipeline.data.accern import alphaone_free as alphaone
# Premium version availabe at
# https://www.quantopian.com/data/accern/alphaone
# from quantopian.pipeline.data.accern import alphaone as alphaone



class PercentSurprise(CustomFactor):
    window_length = 1
    inputs = [ConsensusEstimizeEPS.previous_actual_value,
              ConsensusEstimizeEPS.previous_mean]

    def compute(self, today, assets, out, actual_eps, estimize_eps):
        out[:] = (actual_eps[-1] - estimize_eps[-1])/(estimize_eps[-1] + 0)
        
class DaysSinceRelease(CustomFactor):
    # Only getting the previous quarter's estimize surprise
    window_length = 1
    inputs = [EarningsCalendar.previous_announcement,
              ConsensusEstimizeEPS.previous_release_date]
    
    def compute(self, today, assets, out,
                earnings_announcement, estimize_release):
        days = estimize_release - earnings_announcement
        out[:] = abs(days.astype('timedelta64[D]').astype(int))

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

    # Instantiating our factors  
    factor = PercentSurprise()
    
    # Get the days since estimize release date and the latest
    # Earnings release date
    days_since_release = DaysSinceRelease()
    previous_estimize_release_date = \
        ConsensusEstimizeEPS.previous_release_date.latest

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Filter down to stocks in the top/bottom according to
    # the earnings surprise
    longs = (factor >= context.min_surprise) & (factor <= context.max_surprise)
    shorts = (factor <= -context.min_surprise) & (factor >= -context.max_surprise)

    # Set our pipeline screens  
    # Filter down stocks using sentiment  
    article_sentiment = alphaone.article_sentiment.latest
    top_universe = is_liquid & longs & article_sentiment.notnan() \
        & (article_sentiment > .45)
    bottom_universe = is_liquid & shorts & article_sentiment.notnan() \
        & (article_sentiment < -.45)

    # Add long/shorts to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(bottom_universe, "shorts")
    pipe.add(BusinessDaysSincePreviousEarnings(), 'pe')
    pipe.set_screen((days_since_release <= 7) &
                    (previous_estimize_release_date.notnull()))
    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 = 4
    #: 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 = .06
    
    # Weightage on shorts/long
    context.long_weight = 0.98
    context.short_weight = -0.25

    #: 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_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())
    
  
    
 
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]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]
    context.negative_surprise = assets_in_universe[results.shorts]

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        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
    record(leverage=context.account.leverage)

    # Check our positions for loss or profit and exit if necessary
    check_positions_for_loss_or_profit(context, data)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1  
                if context.stocks_held[security] >= context.days_to_hold:  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]  
            # If we've deleted it but it still hasn't been exited. Try exiting again  
            else:  
                log.info("Haven't yet exited %s, ordering again" % security.symbol)  
                order_target_percent(security, 0)  

    # Check our current 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.tolist() + current_negative_pos
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos
    
    # Rebalance our negative surprise securities (existing + new)
    for security in negative_stocks:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            order_target_percent(security, context.short_weight / 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:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            order_target_percent(security, context.long_weight / 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.current(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 check_positions_for_loss_or_profit(context, data):
    # Sell our positions on longs/shorts for profit or loss
    for security in context.portfolio.positions:
        is_stock_held = context.stocks_held.get(security) >= 0
        if data.can_trade(security) and is_stock_held and not get_open_orders(security):
            current_position = context.portfolio.positions[security].amount  
            cost_basis = context.portfolio.positions[security].cost_basis  
            price = data.current(security, 'price')
            # On Long & Profit
            if price >= cost_basis * 1.07 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Profit')  
                del context.stocks_held[security]  
            # On Short & Profit
            if price <= cost_basis* 0.93 and current_position < 0:
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Profit')  
                del context.stocks_held[security]
            # On Long & Loss
            if price <= cost_basis * 0.96 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Loss')  
                del context.stocks_held[security]  
            # On Short & Loss
            if price >= cost_basis * 1.04 and current_position < 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Loss')  
                del context.stocks_held[security]  
There was a runtime error.

This is the long-only version, the other parameters should be the same as above.

Clone Algorithm
118
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.data.builtin import USEquityPricing
from quantopian.pipeline.factors import CustomFactor, AverageDollarVolume

# Premium version avilable at
# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.estimize import (
    ConsensusEstimizeEPS,
    ConsensusWallstreetEPS,
    ConsensusEstimizeRevenue, 
    ConsensusWallstreetRevenue
)

# 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
)

from quantopian.pipeline.data.accern import alphaone_free as alphaone
# Premium version availabe at
# https://www.quantopian.com/data/accern/alphaone
# from quantopian.pipeline.data.accern import alphaone as alphaone



class PercentSurprise(CustomFactor):
    window_length = 1
    inputs = [ConsensusEstimizeEPS.previous_actual_value,
              ConsensusEstimizeEPS.previous_mean]

    def compute(self, today, assets, out, actual_eps, estimize_eps):
        out[:] = (actual_eps[-1] - estimize_eps[-1])/(estimize_eps[-1] + 0)
        
class DaysSinceRelease(CustomFactor):
    # Only getting the previous quarter's estimize surprise
    window_length = 1
    inputs = [EarningsCalendar.previous_announcement,
              ConsensusEstimizeEPS.previous_release_date]
    
    def compute(self, today, assets, out,
                earnings_announcement, estimize_release):
        days = estimize_release - earnings_announcement
        out[:] = abs(days.astype('timedelta64[D]').astype(int))

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

    # Instantiating our factors  
    factor = PercentSurprise()
    
    # Get the days since estimize release date and the latest
    # Earnings release date
    days_since_release = DaysSinceRelease()
    previous_estimize_release_date = \
        ConsensusEstimizeEPS.previous_release_date.latest

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Filter down to stocks in the top/bottom according to
    # the earnings surprise
    longs = (factor >= context.min_surprise) & (factor <= context.max_surprise)
    shorts = (factor <= -context.min_surprise) & (factor >= -context.max_surprise)

    # Set our pipeline screens  
    # Filter down stocks using sentiment  
    article_sentiment = alphaone.article_sentiment.latest
    top_universe = is_liquid & longs & article_sentiment.notnan() \
        & (article_sentiment > .45)
    bottom_universe = is_liquid & shorts & article_sentiment.notnan() \
        & (article_sentiment < -.45)

    # Add long/shorts to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(bottom_universe, "shorts")
    pipe.add(BusinessDaysSincePreviousEarnings(), 'pe')
    pipe.set_screen((days_since_release <= 7) &
                    (previous_estimize_release_date.notnull()))
    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 = 4
    #: 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 = .06
    
    # Weightage on shorts/long
    context.long_weight = 0.98
    context.short_weight = -0.25

    #: 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_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())
    
  
    
 
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]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]
    context.negative_surprise = assets_in_universe[results.shorts]

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        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
    record(leverage=context.account.leverage)

    # Check our positions for loss or profit and exit if necessary
    check_positions_for_loss_or_profit(context, data)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1  
                if context.stocks_held[security] >= context.days_to_hold:  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]  
            # If we've deleted it but it still hasn't been exited. Try exiting again  
            else:  
                log.info("Haven't yet exited %s, ordering again" % security.symbol)  
                order_target_percent(security, 0)  

    # Check our current 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.tolist() + current_negative_pos
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos
    
    # Rebalance our negative surprise securities (existing + new)
    for security in negative_stocks:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            #order_target_percent(security, context.short_weight / 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:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            order_target_percent(security, context.long_weight / 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.current(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 check_positions_for_loss_or_profit(context, data):
    # Sell our positions on longs/shorts for profit or loss
    for security in context.portfolio.positions:
        is_stock_held = context.stocks_held.get(security) >= 0
        if data.can_trade(security) and is_stock_held and not get_open_orders(security):
            current_position = context.portfolio.positions[security].amount  
            cost_basis = context.portfolio.positions[security].cost_basis  
            price = data.current(security, 'price')
            # On Long & Profit
            if price >= cost_basis * 1.07 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Profit')  
                del context.stocks_held[security]  
            # On Short & Profit
            if price <= cost_basis* 0.93 and current_position < 0:
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Profit')  
                del context.stocks_held[security]
            # On Long & Loss
            if price <= cost_basis * 0.96 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Loss')  
                del context.stocks_held[security]  
            # On Short & Loss
            if price >= cost_basis * 1.04 and current_position < 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Loss')  
                del context.stocks_held[security]  
There was a runtime error.

Great algo Koon,

Here's the out-of-sample results of your algorithm (long/short version) excluding HBI (due to the data issues addressed with Dmitry above).

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 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.data.builtin import USEquityPricing
from quantopian.pipeline.factors import CustomFactor, AverageDollarVolume

# Premium version avilable at
# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.estimize import (
    ConsensusEstimizeEPS,
    ConsensusWallstreetEPS,
    ConsensusEstimizeRevenue, 
    ConsensusWallstreetRevenue
)

# 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
)

# from quantopian.pipeline.data.accern import alphaone_free as alphaone
# Premium version availabe at
# https://www.quantopian.com/data/accern/alphaone
from quantopian.pipeline.data.accern import alphaone as alphaone



class PercentSurprise(CustomFactor):
    window_length = 1
    inputs = [ConsensusEstimizeEPS.previous_actual_value,
              ConsensusEstimizeEPS.previous_mean]

    def compute(self, today, assets, out, actual_eps, estimize_eps):
        out[:] = (actual_eps[-1] - estimize_eps[-1])/(estimize_eps[-1] + 0)
        
class DaysSinceRelease(CustomFactor):
    # Only getting the previous quarter's estimize surprise
    window_length = 1
    inputs = [EarningsCalendar.previous_announcement,
              ConsensusEstimizeEPS.previous_release_date]
    
    def compute(self, today, assets, out,
                earnings_announcement, estimize_release):
        days = estimize_release - earnings_announcement
        out[:] = abs(days.astype('timedelta64[D]').astype(int))

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

    # Instantiating our factors  
    factor = PercentSurprise()
    
    # Get the days since estimize release date and the latest
    # Earnings release date
    days_since_release = DaysSinceRelease()
    previous_estimize_release_date = \
        ConsensusEstimizeEPS.previous_release_date.latest

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Filter down to stocks in the top/bottom according to
    # the earnings surprise
    longs = (factor >= context.min_surprise) & (factor <= context.max_surprise)
    shorts = (factor <= -context.min_surprise) & (factor >= -context.max_surprise)

    # Set our pipeline screens  
    # Filter down stocks using sentiment  
    article_sentiment = alphaone.article_sentiment.latest
    top_universe = is_liquid & longs & article_sentiment.notnan() \
        & (article_sentiment > .45)
    bottom_universe = is_liquid & shorts & article_sentiment.notnan() \
        & (article_sentiment < -.45)

    # Add long/shorts to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(bottom_universe, "shorts")
    pipe.add(BusinessDaysSincePreviousEarnings(), 'pe')
    pipe.set_screen((days_since_release <= 7) &
                    (previous_estimize_release_date.notnull()))
    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 = 4
    #: 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 = .06
    
    # Weightage on shorts/long
    context.long_weight = 0.98
    context.short_weight = -0.25

    #: 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_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())
    
  
    
 
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]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]
    context.negative_surprise = assets_in_universe[results.shorts]

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        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
    record(leverage=context.account.leverage)

    # Check our positions for loss or profit and exit if necessary
    check_positions_for_loss_or_profit(context, data)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1  
                if context.stocks_held[security] >= context.days_to_hold:  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]  
            # If we've deleted it but it still hasn't been exited. Try exiting again  
            else:  
                log.info("Haven't yet exited %s, ordering again" % security.symbol)  
                order_target_percent(security, 0)  

    # Check our current 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.tolist() + current_negative_pos
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos
    
    # Rebalance our negative surprise securities (existing + new)
    for security in negative_stocks:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade and security != sid(32497):
            order_target_percent(security, context.short_weight / 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:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade and security != sid(32497):
            order_target_percent(security, context.long_weight / 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.current(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 check_positions_for_loss_or_profit(context, data):
    # Sell our positions on longs/shorts for profit or loss
    for security in context.portfolio.positions:
        is_stock_held = context.stocks_held.get(security) >= 0
        if data.can_trade(security) and is_stock_held and not get_open_orders(security):
            current_position = context.portfolio.positions[security].amount  
            cost_basis = context.portfolio.positions[security].cost_basis  
            price = data.current(security, 'price')
            # On Long & Profit
            if price >= cost_basis * 1.07 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Profit')  
                del context.stocks_held[security]  
            # On Short & Profit
            if price <= cost_basis* 0.93 and current_position < 0:
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Profit')  
                del context.stocks_held[security]
            # On Long & Loss
            if price <= cost_basis * 0.96 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Loss')  
                del context.stocks_held[security]  
            # On Short & Loss
            if price >= cost_basis * 1.04 and current_position < 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Loss')  
                del context.stocks_held[security]  
There was a runtime error.

How would it perform over a longer period of years? Like 10 years? I don't have the subscription to run longer test yet, would appreciate if you could post one.

Thank you.

Hi Koon,

I've run this over the timeframe that the dataset is available for (Accern is available starting October 2012 ~ present day with full subscription). I've also excluded HBI from the list of securities.

Beta is a little high so if your goal is to enter this in the contest, it may need some work adjusting the weight on short positions.

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 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.data.builtin import USEquityPricing
from quantopian.pipeline.factors import CustomFactor, AverageDollarVolume

# Premium version avilable at
# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.estimize import (
    ConsensusEstimizeEPS,
    ConsensusWallstreetEPS,
    ConsensusEstimizeRevenue, 
    ConsensusWallstreetRevenue
)

# 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
)

# from quantopian.pipeline.data.accern import alphaone_free as alphaone
# Premium version availabe at
# https://www.quantopian.com/data/accern/alphaone
from quantopian.pipeline.data.accern import alphaone as alphaone



class PercentSurprise(CustomFactor):
    window_length = 1
    inputs = [ConsensusEstimizeEPS.previous_actual_value,
              ConsensusEstimizeEPS.previous_mean]

    def compute(self, today, assets, out, actual_eps, estimize_eps):
        out[:] = (actual_eps[-1] - estimize_eps[-1])/(estimize_eps[-1] + 0)
        
class DaysSinceRelease(CustomFactor):
    # Only getting the previous quarter's estimize surprise
    window_length = 1
    inputs = [EarningsCalendar.previous_announcement,
              ConsensusEstimizeEPS.previous_release_date]
    
    def compute(self, today, assets, out,
                earnings_announcement, estimize_release):
        days = estimize_release - earnings_announcement
        out[:] = abs(days.astype('timedelta64[D]').astype(int))

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

    # Instantiating our factors  
    factor = PercentSurprise()
    
    # Get the days since estimize release date and the latest
    # Earnings release date
    days_since_release = DaysSinceRelease()
    previous_estimize_release_date = \
        ConsensusEstimizeEPS.previous_release_date.latest

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Filter down to stocks in the top/bottom according to
    # the earnings surprise
    longs = (factor >= context.min_surprise) & (factor <= context.max_surprise)
    shorts = (factor <= -context.min_surprise) & (factor >= -context.max_surprise)

    # Set our pipeline screens  
    # Filter down stocks using sentiment  
    article_sentiment = alphaone.article_sentiment.latest
    top_universe = is_liquid & longs & article_sentiment.notnan() \
        & (article_sentiment > .45)
    bottom_universe = is_liquid & shorts & article_sentiment.notnan() \
        & (article_sentiment < -.45)

    # Add long/shorts to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(bottom_universe, "shorts")
    pipe.add(BusinessDaysSincePreviousEarnings(), 'pe')
    pipe.set_screen((days_since_release <= 7) &
                    (previous_estimize_release_date.notnull()))
    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 = 4
    #: 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 = .06
    
    # Weightage on shorts/long
    context.long_weight = 0.98
    context.short_weight = -0.25

    #: 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_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())
    
  
    
 
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]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]
    context.negative_surprise = assets_in_universe[results.shorts]

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        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
    record(leverage=context.account.leverage)

    # Check our positions for loss or profit and exit if necessary
    check_positions_for_loss_or_profit(context, data)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1  
                if context.stocks_held[security] >= context.days_to_hold:  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]  
            # If we've deleted it but it still hasn't been exited. Try exiting again  
            else:  
                log.info("Haven't yet exited %s, ordering again" % security.symbol)  
                order_target_percent(security, 0)  

    # Check our current 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.tolist() + current_negative_pos
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos
    
    # Rebalance our negative surprise securities (existing + new)
    for security in negative_stocks:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade and security != sid(32497):
            order_target_percent(security, context.short_weight / 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:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade and security != sid(32497):
            order_target_percent(security, context.long_weight / 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.current(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 check_positions_for_loss_or_profit(context, data):
    # Sell our positions on longs/shorts for profit or loss
    for security in context.portfolio.positions:
        is_stock_held = context.stocks_held.get(security) >= 0
        if data.can_trade(security) and is_stock_held and not get_open_orders(security):
            current_position = context.portfolio.positions[security].amount  
            cost_basis = context.portfolio.positions[security].cost_basis  
            price = data.current(security, 'price')
            # On Long & Profit
            if price >= cost_basis * 1.07 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Profit')  
                del context.stocks_held[security]  
            # On Short & Profit
            if price <= cost_basis* 0.93 and current_position < 0:
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Profit')  
                del context.stocks_held[security]
            # On Long & Loss
            if price <= cost_basis * 0.96 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Loss')  
                del context.stocks_held[security]  
            # On Short & Loss
            if price >= cost_basis * 1.04 and current_position < 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Loss')  
                del context.stocks_held[security]  
There was a runtime error.

Hi,

Thanks Seong Lee.

In make_pipeline, I tried to add the below line
b_profitable = morningstar.asset_classification.growth_grade == "A"

The compiler returned:
NotPipelineCompatible: 0079 asset_classification.profitability_grade is not currently available in the Pipeline API

How do I use get_fundamentals() alongside pipeline? get_fundamentals() has to be used inside before_trading_start. Below is the before_trading_start() in the code you provided.

def before_trading_start(context, data):
results = pipeline_output('estimize')
results = results[results['pe'] == 1]
assets_in_universe = results.index
context.positive_surprise = assets_in_universe[results.longs]
context.negative_surprise = assets_in_universe[results.shorts]

Could you provide an example on how to use the get_fundamental() to create a filter in before_trading_start()?

Hi Koon,

We're actively working on a feature that will allow you to use profitability grade (and other string based fundamental data points) in pipeline. Keep your eyes on the forums for an announcement.

Thanks
Josh

Disclaimer

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.

I am trying to trade this Algo via Paper trade and it has not traded for the last week. When back testing I see trades however. I am subscripted to the datasets. Any help would be appreciated.

Hi Dennis, here is a backtest during that same time period and it appears that none of the earnings announcements met the trade criteria:

2016-05-17 before_trading_start:140 INFO                       longs  pe shorts  
Equity(110 [ACXM])    False   1  False  
Equity(3496 [HD])     False   1  False  
Equity(7457 [TJX])    False   1  False  
Equity(8229 [WMT])    False   1  False  
Equity(23904 [RRGB])  False   1  False  
Equity(24789 [PLCE])  False   1  False  
2016-05-18 before_trading_start:140 INFO                       longs  pe shorts  
Equity(122 [ADI])     False   1  False  
Equity(1900 [CSCO])   False   1  False  
Equity(2876 [FLO])    False   1  False  
Equity(3668 [HRL])    False   1  False  
Equity(4521 [LOW])    False   1  False  
Equity(6018 [PLAB])   False   1  False  
Equity(6994 [SNPS])   False   1  False  
Equity(7061 [SPLS])   False   1  False  
Equity(7173 [STE])    False   1  False  
Equity(11086 [AEO])   False   1  False  
Equity(11120 [EXP])   False   1  False  
Equity(16820 [TTWO])  False   1  False  
Equity(21090 [TGT])   False   1  False  
Equity(26401 [CRM])   False   1  False  
Equity(27251 [CTRN])  False   1  False  
Equity(40420 [BAH])   False   1  False  
Equity(42738 [RXN])   False   1  False  
Equity(47980 [BOOT])  False   1  False  
2016-05-19 before_trading_start:140 INFO                       longs  pe shorts  
Equity(337 [AMAT])    False   1  False  
Equity(1078 [BRC])    False   1  False  
Equity(3321 [GPS])    False   1  False  
Equity(4794 [MENT])   False   1  False  
Equity(4945 [MNRO])   False   1  False  
Equity(6311 [QSII])   False   1  False  
Equity(6546 [ROST])   False   1  False  
Equity(6969 [SMRT])   False   1  False  
Equity(7623 [TTC])    False   1  False  
Equity(8229 [WMT])    False   1  False  
Equity(8733 [SCVL])   False   1  False  
Equity(13289 [TK])    False   1  False  
Equity(20061 [BRCD])  False   1  False  
Equity(20269 [PERY])  False   1  False  
Equity(22889 [EGHT])  False   1  False  
Equity(23175 [AAP])   False   1  False  
Equity(23395 [SSI])   False   1  False  
Equity(24070 [DKS])   False   1  False  
Equity(26542 [ELOS])  False   1  False  
Equity(27224 [TGP])   False   1  False  
Equity(33041 [TOO])   False   1  False  
Equity(34277 [HGG])   False   1  False  
2016-05-22 before_trading_start:140 INFO                       longs  pe shorts  
Equity(915 [BKE])     False   1  False  
Equity(1795 [CPB])    False   1  False  
Equity(8383 [FL])     False   1  False  
Equity(15815 [HIBB])  False   1  False  
2016-05-23 before_trading_start:140 INFO                      longs  pe shorts  
Equity(5253 [NDSN])  False   1  False  
2016-05-24 before_trading_start:140 INFO                       longs  pe shorts  
Equity(693 [AZO])     False   1  False  
Equity(1898 [CSC])    False   1  False  
Equity(2385 [DY])     False   1  False  
Equity(7530 [TOL])    False   1  False  
Equity(7895 [VAL])    False   1  False  
Equity(8655 [INTU])   False   1  False  
Equity(16307 [VSAT])  False   1  False  
Equity(23873 [KIRK])  False   1  False  
Equity(27409 [DSW])   False   1  False  
Equity(46002 [NMBL])  False   1  False  
Equity(46777 [SPWH])  False   1  False  
Equity(46876 [TOUR])  False   1  False  
Equity(49506 [HPE])   False   1  False  
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 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.data.builtin import USEquityPricing
from quantopian.pipeline.factors import CustomFactor, AverageDollarVolume

# Premium version avilable at
# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.estimize import (
    ConsensusEstimizeEPS,
    ConsensusWallstreetEPS,
    ConsensusEstimizeRevenue, 
    ConsensusWallstreetRevenue
)

# 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
)

# from quantopian.pipeline.data.accern import alphaone_free as alphaone
# Premium version availabe at
# https://www.quantopian.com/data/accern/alphaone
from quantopian.pipeline.data.accern import alphaone as alphaone

class PercentSurprise(CustomFactor):
    window_length = 1
    inputs = [ConsensusEstimizeEPS.previous_actual_value,
              ConsensusEstimizeEPS.previous_mean]

    def compute(self, today, assets, out, actual_eps, estimize_eps):
        out[:] = (actual_eps[-1] - estimize_eps[-1])/(estimize_eps[-1] + 0)
        
class DaysSinceRelease(CustomFactor):
    # Only getting the previous quarter's estimize surprise
    window_length = 1
    inputs = [EarningsCalendar.previous_announcement,
              ConsensusEstimizeEPS.previous_release_date]
    
    def compute(self, today, assets, out,
                earnings_announcement, estimize_release):
        days = estimize_release - earnings_announcement
        out[:] = abs(days.astype('timedelta64[D]').astype(int))

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

    # Instantiating our factors  
    factor = PercentSurprise()
    
    # Get the days since estimize release date and the latest
    # Earnings release date
    days_since_release = DaysSinceRelease()
    previous_estimize_release_date = \
        ConsensusEstimizeEPS.previous_release_date.latest

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Filter down to stocks in the top/bottom according to
    # the earnings surprise
    longs = (factor >= context.min_surprise) & (factor <= context.max_surprise)
    shorts = (factor <= -context.min_surprise) & (factor >= -context.max_surprise)

    # Set our pipeline screens  
    # Filter down stocks using sentiment  
    article_sentiment = alphaone.article_sentiment.latest
    top_universe = is_liquid & longs & article_sentiment.notnan() \
        & (article_sentiment > .45)
    bottom_universe = is_liquid & shorts & article_sentiment.notnan() \
        & (article_sentiment < -.45)

    # Add long/shorts to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(bottom_universe, "shorts")
    pipe.add(BusinessDaysSincePreviousEarnings(), 'pe')
    pipe.set_screen((days_since_release <= 7) &
                    (previous_estimize_release_date.notnull()))
    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 = 4
    #: 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 = .06

    #: 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_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())

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]
    log.info(results)
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]
    context.negative_surprise = assets_in_universe[results.shorts]

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        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
    record(leverage=context.account.leverage)

    # Check our positions for loss or profit and exit if necessary
    check_positions_for_loss_or_profit(context, data)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1  
                if context.stocks_held[security] >= context.days_to_hold:  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]  
            # If we've deleted it but it still hasn't been exited. Try exiting again  
            else:  
                log.info("Haven't yet exited %s, ordering again" % security.symbol)  
                order_target_percent(security, 0)  

    # Check our current 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.tolist() + current_negative_pos
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos
    
    # Rebalance our negative surprise securities (existing + new)
    for security in negative_stocks:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            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:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            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.current(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 check_positions_for_loss_or_profit(context, data):
    # Sell our positions on longs/shorts for profit or loss
    for security in context.portfolio.positions:
        is_stock_held = context.stocks_held.get(security) >= 0
        if data.can_trade(security) and is_stock_held and not get_open_orders(security):
            current_position = context.portfolio.positions[security].amount  
            cost_basis = context.portfolio.positions[security].cost_basis  
            price = data.current(security, 'price')
            # On Long & Profit
            if price >= cost_basis * 1.06 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Profit')  
                del context.stocks_held[security]  
            # On Short & Profit
            if price <= cost_basis* 0.94 and current_position < 0:
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Profit')  
                del context.stocks_held[security]
            # On Long & Loss
            if price <= cost_basis * 0.94 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Loss')  
                del context.stocks_held[security]  
            # On Short & Loss
            if price >= cost_basis * 1.06 and current_position < 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Loss')  
                del context.stocks_held[security]
There was a runtime error.

Hey Guys (this is my first post in the forums!),
So I put more work into this than I care to admit, but I've corrected a few issues here:
-The issue of the algorithm entering short positions when you've only weighted for long that Steven Hayes identified
-Fixed how surprise was calculated for negative earnings as it was providing incorrect information which caused shorts to enter incorrect positions
-Fixed an issue where it was investing too deeply into securities that it couldn't easily liquidate out of when portfolios got too large
-Optimized the days held and weightings

I think investing into each valid position equally is not the best idea in the end, I'm currently toying with the idea of adding a new signal into the pipeline to sort the strength of each position and weight our entries amounts by that signal. Hopefully someone else can build off this.

Clone Algorithm
212
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
"""
PEAD (Post Earnings Announcement Drift)
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

Relevant Studies:
http://people.brandeis.edu/~heidifox/ese.pdf
and
http://papers.ssrn.com/sol3/papers.cfm?abstract_id=1783049
"""

import numpy as np

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, AverageDollarVolume


# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.estimize import (
    ConsensusEstimizeEPS,
    ConsensusWallstreetEPS,
    ConsensusEstimizeRevenue, 
    ConsensusWallstreetRevenue
)


# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.eventvestor import EarningsCalendar
from quantopian.pipeline.factors.eventvestor import (
    BusinessDaysUntilNextEarnings,
    BusinessDaysSincePreviousEarnings
)

# https://www.quantopian.com/data/accern/alphaone
from quantopian.pipeline.data.accern import alphaone

class PercentSurprise(CustomFactor):
    window_length = 1
    inputs = [ConsensusEstimizeEPS.previous_actual_value,
              ConsensusEstimizeEPS.previous_mean]

    def compute(self, today, assets, out, actual_eps, estimize_eps):
        out[:] = (actual_eps[-1] - estimize_eps[-1])/abs((estimize_eps[-1] + 0))
        
class DaysSinceRelease(CustomFactor):
    # Only getting the previous quarter's estimize surprise
    window_length = 1
    inputs = [EarningsCalendar.previous_announcement,
              ConsensusEstimizeEPS.previous_release_date]
    
    def compute(self, today, assets, out,
                earnings_announcement, estimize_release):
        days = estimize_release - earnings_announcement
        out[:] = abs(days.astype('timedelta64[D]').astype(int))

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

    # Instantiating our factors  
    factor = PercentSurprise()
    
    # Get the days since estimize release date and the latest
    # Earnings release date
    days_since_release = DaysSinceRelease()
    previous_estimize_release_date = \
        ConsensusEstimizeEPS.previous_release_date.latest

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Filter down to stocks in the top/bottom according to
    # the earnings surprise
    longs = (factor >= context.min_positive_surprise) & (factor <= context.max_positive_surprise)
    shorts = (factor <= context.min_negative_surprise) & (factor >= context.max_negative_surprise)

    # Set our pipeline screens  
    # Filter down stocks using sentiment  
    article_sentiment = alphaone.article_sentiment.latest
    top_universe = is_liquid & longs & article_sentiment.notnan() \
        & (article_sentiment > .45)
    bottom_universe = is_liquid & shorts & article_sentiment.notnan() \
        & (article_sentiment < -.45)

    # Add long/shorts to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(bottom_universe, "shorts")
    pipe.add(BusinessDaysSincePreviousEarnings(), 'pe')
    pipe.set_screen((days_since_release <= 7) &
                    (previous_estimize_release_date.notnull()))
    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_short = 2
    context.days_to_hold_long = 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
    #: Negative surprise Delta must be a negative float
    #: Positive surprise Delta must be a positive float
    context.min_negative_surprise = -0.5
    context.max_negative_surprise = -1
    context.min_positive_surprise = 0.00
    context.max_positive_surprise = 0.055
    
    context.volume_limit = 0.0125
    
    #: Weightage on shorts/long 
    #: MUST BE FLOAT
    #: Short weight must be negative
    context.long_weight = 0.95 #5% cash
    context.short_weight = -0.25
    
    # 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_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())
    
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]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]
    context.negative_surprise = assets_in_universe[results.shorts]

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        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
    record(leverage=context.account.leverage)

    # Check our positions for loss or profit and exit if necessary
    check_positions_for_loss_or_profit(context, data)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1
                share = port[security].amount
                if (context.stocks_held[security] > context.days_to_hold_long) and (share > 0):  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]  
                elif (context.stocks_held[security] > context.days_to_hold_short) and (share < 0):  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]
                
    # Check our current 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.tolist() + current_negative_pos
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos
    
    # Rebalance our negative surprise securities (existing + new)
    for security in negative_stocks:
        price = data.current(security, 'price')
        volume_hist = data.history(security, 'volume', bar_count=30, frequency='1d')
        volume = volume_hist.mean()
        can_trade = (context.stocks_held.get(security) <= context.days_to_hold_short) or (context.stocks_held.get(security) is None)
        if data.can_trade(security) and can_trade:
            order_pct_neg = max(context.short_weight / len(negative_stocks), -1.0)
            can_buy = (volume * context.volume_limit) > (context.portfolio.portfolio_value * order_pct_neg)/price
            if can_buy:
                order_target_percent(security, order_pct_neg)
                if context.stocks_held.get(security) is None:
                    context.stocks_held[security] = 0
            elif not can_buy:
                order_pct_neg = (volume * context.volume_limit * price)/context.portfolio.portfolio_value
                order_target_percent(security, order_pct_neg)
                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:
        price = data.current(security, 'price')
        volume_hist = data.history(security, 'volume', bar_count=30, frequency='1d')
        volume = volume_hist.mean()
        can_trade = (context.stocks_held.get(security) <= context.days_to_hold_long) or (context.stocks_held.get(security) is None)
        if data.can_trade(security) and can_trade:
            order_pct_pos = min(context.long_weight / len(positive_stocks), 1.0)
            can_buy = (volume * context.volume_limit) > (context.portfolio.portfolio_value * order_pct_pos)/price
            if can_buy:
                order_target_percent(security, order_pct_pos)
                if context.stocks_held.get(security) is None:
                    context.stocks_held[security] = 0
            elif not can_buy:
                order_pct_pos = (volume * context.volume_limit * price)/context.portfolio.portfolio_value
                order_target_percent(security, order_pct_pos)
                if context.stocks_held.get(security) is None:
                    context.stocks_held[security] = 0
    
def check_positions_for_loss_or_profit(context, data):
    # Sell our positions on longs/shorts for profit or loss
    for security in context.portfolio.positions:
        is_stock_held = context.stocks_held.get(security) >= 0
        if data.can_trade(security) and is_stock_held and not get_open_orders(security):
            current_position = context.portfolio.positions[security].amount  
            cost_basis = context.portfolio.positions[security].cost_basis  
            price = data.current(security, 'price')
            # On Long & Profit
            if price >= cost_basis * 1.06 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Profit')  
                del context.stocks_held[security]  
            # On Short & Profit
            if price <= cost_basis* 0.93 and current_position < 0:
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Profit')  
                del context.stocks_held[security]
            # On Long & Loss
            if price <= cost_basis * 0.97 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Loss')  
                del context.stocks_held[security]  
            # On Short & Loss
            if price >= cost_basis * 1.04 and current_position < 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Loss')  
                del context.stocks_held[security]
There was a runtime error.

Quick Update to previous algorithm, it actually needs even further reduction in investment with large portfolios so I pegged it down to a flat 1% (from 1.25%) of the mean trading volume over the past 30 days.

I also added a couple lines to invest into SPY when not investing into any other long positions. Since earnings reports all come out generally around the same time and there's a lot of downtime for the algorithm, why not place the money somewhere more productive? This could be elaborated on.

Clone Algorithm
212
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
"""
PEAD (Post Earnings Announcement Drift)
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

Relevant Studies:
http://people.brandeis.edu/~heidifox/ese.pdf
and
http://papers.ssrn.com/sol3/papers.cfm?abstract_id=1783049
"""

import numpy as np

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, AverageDollarVolume


# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.estimize import (
    ConsensusEstimizeEPS,
    ConsensusWallstreetEPS,
    ConsensusEstimizeRevenue, 
    ConsensusWallstreetRevenue
)


# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.eventvestor import EarningsCalendar
from quantopian.pipeline.factors.eventvestor import (
    BusinessDaysUntilNextEarnings,
    BusinessDaysSincePreviousEarnings
)

# https://www.quantopian.com/data/accern/alphaone
from quantopian.pipeline.data.accern import alphaone

class PercentSurprise(CustomFactor):
    window_length = 1
    inputs = [ConsensusEstimizeEPS.previous_actual_value,
              ConsensusEstimizeEPS.previous_mean]

    def compute(self, today, assets, out, actual_eps, estimize_eps):
        out[:] = (actual_eps[-1] - estimize_eps[-1])/abs((estimize_eps[-1] + 0))
        
class DaysSinceRelease(CustomFactor):
    # Only getting the previous quarter's estimize surprise
    window_length = 1
    inputs = [EarningsCalendar.previous_announcement,
              ConsensusEstimizeEPS.previous_release_date]
    
    def compute(self, today, assets, out,
                earnings_announcement, estimize_release):
        days = estimize_release - earnings_announcement
        out[:] = abs(days.astype('timedelta64[D]').astype(int))

def make_pipeline(context):
    # Create our pipeline  
    pipe = Pipeline()
    
    # Instantiating our factors  
    factor = PercentSurprise()
    
    # Get the days since estimize release date and the latest
    # Earnings release date
    days_since_release = DaysSinceRelease()
    previous_estimize_release_date = \
        ConsensusEstimizeEPS.previous_release_date.latest

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Filter down to stocks in the top/bottom according to
    # the earnings surprise
    longs = (factor >= context.min_positive_surprise) & (factor <= context.max_positive_surprise)
    shorts = (factor <= context.min_negative_surprise) & (factor >= context.max_negative_surprise)

    # Set our pipeline screens  
    # Filter down stocks using sentiment  
    article_sentiment = alphaone.article_sentiment.latest
    top_universe = is_liquid & longs & article_sentiment.notnan() \
        & (article_sentiment > .45)
    bottom_universe = is_liquid & shorts & article_sentiment.notnan() \
        & (article_sentiment < -.45)

    # Add long/shorts to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(bottom_universe, "shorts")
    pipe.add(BusinessDaysSincePreviousEarnings(), 'pe')
    pipe.set_screen((days_since_release <= 7) &
                    (previous_estimize_release_date.notnull()))
    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))

    #Setup our hedge account
    context.hedgeAccount = symbol('SPY')

    #: Declaring the days to hold, change this to what you want
    context.days_to_hold_short = 2
    context.days_to_hold_long = 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
    #: Negative surprise Delta must be a negative float
    #: Positive surprise Delta must be a positive float
    context.min_negative_surprise = -0.5
    context.max_negative_surprise = -1.0
    context.min_positive_surprise = 0.00
    context.max_positive_surprise = 0.055
    
    context.volume_limit = 0.01
    
    #: Weightage on shorts/long 
    #: MUST BE FLOAT
    #: Short weight must be negative
    context.long_weight = 0.95 #5% cash
    context.short_weight = -0.25
    
    # 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_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())
    
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]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]
    context.negative_surprise = assets_in_universe[results.shorts]

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        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
    record(leverage=context.account.leverage)

    # Check our positions for loss or profit and exit if necessary
    check_positions_for_loss_or_profit(context, data)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1
                share = port[security].amount
                if (context.stocks_held[security] > context.days_to_hold_long) and (share > 0):  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]  
                elif (context.stocks_held[security] > context.days_to_hold_short) and (share < 0):  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]
                
    # Check our current 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.tolist() + current_negative_pos
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos
    
    if positive_stocks:
        order_target_percent(context.hedgeAccount, 0)
    elif not positive_stocks:
        order_target_percent(context.hedgeAccount, 1.0)
    
    # Rebalance our negative surprise securities (existing + new)
    for security in negative_stocks:
        price = data.current(security, 'price')
        volume_hist = data.history(security, 'volume', bar_count=30, frequency='1d')
        volume = volume_hist.mean()
        can_trade = (context.stocks_held.get(security) <= context.days_to_hold_short) or (context.stocks_held.get(security) is None)
        if data.can_trade(security) and can_trade:
            order_pct_neg = max(context.short_weight / len(negative_stocks), -1.0)
            can_buy = (volume * context.volume_limit) > (context.portfolio.portfolio_value * order_pct_neg)/price
            if can_buy:
                order_target_percent(security, order_pct_neg)
                if context.stocks_held.get(security) is None:
                    context.stocks_held[security] = 0
            elif not can_buy:
                order_pct_neg = (volume * context.volume_limit * price)/context.portfolio.portfolio_value
                order_target_percent(security, order_pct_neg)
                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:
        price = data.current(security, 'price')
        volume_hist = data.history(security, 'volume', bar_count=30, frequency='1d')
        volume = volume_hist.mean()
        can_trade = (context.stocks_held.get(security) <= context.days_to_hold_long) or (context.stocks_held.get(security) is None)
        if data.can_trade(security) and can_trade:
            order_pct_pos = min(context.long_weight / len(positive_stocks), 1.0)
            can_buy = (volume * context.volume_limit) > (context.portfolio.portfolio_value * order_pct_pos)/price
            if can_buy:
                order_target_percent(security, order_pct_pos)
                if context.stocks_held.get(security) is None:
                    context.stocks_held[security] = 0
            elif not can_buy:
                order_pct_pos = (volume * context.volume_limit * price)/context.portfolio.portfolio_value
                order_target_percent(security, order_pct_pos)
                if context.stocks_held.get(security) is None:
                    context.stocks_held[security] = 0
    
def check_positions_for_loss_or_profit(context, data):
    # Sell our positions on longs/shorts for profit or loss
    for security in context.portfolio.positions:
        is_stock_held = context.stocks_held.get(security) >= 0
        if data.can_trade(security) and is_stock_held and not get_open_orders(security):
            current_position = context.portfolio.positions[security].amount  
            cost_basis = context.portfolio.positions[security].cost_basis  
            price = data.current(security, 'price')
            # On Long & Profit
            if price >= cost_basis * 1.06 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Profit')  
                del context.stocks_held[security]  
            # On Short & Profit
            if price <= cost_basis* 0.93 and current_position < 0:
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Profit')  
                del context.stocks_held[security]
            # On Long & Loss
            if price <= cost_basis * 0.97 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Loss')  
                del context.stocks_held[security]  
            # On Short & Loss
            if price >= cost_basis * 1.04 and current_position < 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Loss')  
                del context.stocks_held[security]
There was a runtime error.

Lyth,

Here's the same algorithm, but without the inclusion of HBI into your universe as there have been some data issues with that security.

Clone Algorithm
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
"""
PEAD (Post Earnings Announcement Drift)
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

Relevant Studies:
http://people.brandeis.edu/~heidifox/ese.pdf
and
http://papers.ssrn.com/sol3/papers.cfm?abstract_id=1783049
"""

import numpy as np

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, AverageDollarVolume


# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.estimize import (
    ConsensusEstimizeEPS,
    ConsensusWallstreetEPS,
    ConsensusEstimizeRevenue, 
    ConsensusWallstreetRevenue
)


# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.eventvestor import EarningsCalendar
from quantopian.pipeline.factors.eventvestor import (
    BusinessDaysUntilNextEarnings,
    BusinessDaysSincePreviousEarnings
)

# https://www.quantopian.com/data/accern/alphaone
from quantopian.pipeline.data.accern import alphaone

class PercentSurprise(CustomFactor):
    window_length = 1
    inputs = [ConsensusEstimizeEPS.previous_actual_value,
              ConsensusEstimizeEPS.previous_mean]

    def compute(self, today, assets, out, actual_eps, estimize_eps):
        out[:] = (actual_eps[-1] - estimize_eps[-1])/abs((estimize_eps[-1] + 0))
        
class DaysSinceRelease(CustomFactor):
    # Only getting the previous quarter's estimize surprise
    window_length = 1
    inputs = [EarningsCalendar.previous_announcement,
              ConsensusEstimizeEPS.previous_release_date]
    
    def compute(self, today, assets, out,
                earnings_announcement, estimize_release):
        days = estimize_release - earnings_announcement
        out[:] = abs(days.astype('timedelta64[D]').astype(int))

def make_pipeline(context):
    # Create our pipeline  
    pipe = Pipeline()
    
    # Instantiating our factors  
    factor = PercentSurprise()
    
    # Get the days since estimize release date and the latest
    # Earnings release date
    days_since_release = DaysSinceRelease()
    previous_estimize_release_date = \
        ConsensusEstimizeEPS.previous_release_date.latest

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Filter down to stocks in the top/bottom according to
    # the earnings surprise
    longs = (factor >= context.min_positive_surprise) & (factor <= context.max_positive_surprise)
    shorts = (factor <= context.min_negative_surprise) & (factor >= context.max_negative_surprise)

    # Set our pipeline screens  
    # Filter down stocks using sentiment  
    article_sentiment = alphaone.article_sentiment.latest
    top_universe = is_liquid & longs & article_sentiment.notnan() \
        & (article_sentiment > .45)
    bottom_universe = is_liquid & shorts & article_sentiment.notnan() \
        & (article_sentiment < -.45)

    # Add long/shorts to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(bottom_universe, "shorts")
    pipe.add(BusinessDaysSincePreviousEarnings(), 'pe')
    pipe.set_screen((days_since_release <= 7) &
                    (previous_estimize_release_date.notnull()))
    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))

    #Setup our hedge account
    context.hedgeAccount = symbol('SPY')

    #: Declaring the days to hold, change this to what you want
    context.days_to_hold_short = 2
    context.days_to_hold_long = 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
    #: Negative surprise Delta must be a negative float
    #: Positive surprise Delta must be a positive float
    context.min_negative_surprise = -0.5
    context.max_negative_surprise = -1.0
    context.min_positive_surprise = 0.00
    context.max_positive_surprise = 0.055
    
    context.volume_limit = 0.01
    
    #: Weightage on shorts/long 
    #: MUST BE FLOAT
    #: Short weight must be negative
    context.long_weight = 0.95 #5% cash
    context.short_weight = -0.25
    
    # 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_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())
    
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]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]
    context.negative_surprise = assets_in_universe[results.shorts]

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        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
    record(leverage=context.account.leverage)

    # Check our positions for loss or profit and exit if necessary
    check_positions_for_loss_or_profit(context, data)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1
                share = port[security].amount
                if (context.stocks_held[security] > context.days_to_hold_long) and (share > 0):  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]  
                elif (context.stocks_held[security] > context.days_to_hold_short) and (share < 0):  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]
                
    # Check our current 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.tolist() + current_negative_pos
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos
    
    if positive_stocks:
        order_target_percent(context.hedgeAccount, 0)
    elif not positive_stocks:
        order_target_percent(context.hedgeAccount, 1.0)
    
    # Rebalance our negative surprise securities (existing + new)
    for security in negative_stocks:
        price = data.current(security, 'price')
        volume_hist = data.history(security, 'volume', bar_count=30, frequency='1d')
        volume = volume_hist.mean()
        can_trade = (context.stocks_held.get(security) <= context.days_to_hold_short) or (context.stocks_held.get(security) is None)
        if data.can_trade(security) and can_trade and security != sid(32497):
            order_pct_neg = max(context.short_weight / len(negative_stocks), -1.0)
            can_buy = (volume * context.volume_limit) > (context.portfolio.portfolio_value * order_pct_neg)/price
            if can_buy:
                order_target_percent(security, order_pct_neg)
                if context.stocks_held.get(security) is None:
                    context.stocks_held[security] = 0
            elif not can_buy:
                order_pct_neg = (volume * context.volume_limit * price)/context.portfolio.portfolio_value
                order_target_percent(security, order_pct_neg)
                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:
        price = data.current(security, 'price')
        volume_hist = data.history(security, 'volume', bar_count=30, frequency='1d')
        volume = volume_hist.mean()
        can_trade = (context.stocks_held.get(security) <= context.days_to_hold_long) or (context.stocks_held.get(security) is None)
        if data.can_trade(security) and can_trade and security != sid(32497):
            order_pct_pos = min(context.long_weight / len(positive_stocks), 1.0)
            can_buy = (volume * context.volume_limit) > (context.portfolio.portfolio_value * order_pct_pos)/price
            if can_buy:
                order_target_percent(security, order_pct_pos)
                if context.stocks_held.get(security) is None:
                    context.stocks_held[security] = 0
            elif not can_buy:
                order_pct_pos = (volume * context.volume_limit * price)/context.portfolio.portfolio_value
                order_target_percent(security, order_pct_pos)
                if context.stocks_held.get(security) is None:
                    context.stocks_held[security] = 0
    
def check_positions_for_loss_or_profit(context, data):
    # Sell our positions on longs/shorts for profit or loss
    for security in context.portfolio.positions:
        is_stock_held = context.stocks_held.get(security) >= 0
        if data.can_trade(security) and is_stock_held and not get_open_orders(security):
            current_position = context.portfolio.positions[security].amount  
            cost_basis = context.portfolio.positions[security].cost_basis  
            price = data.current(security, 'price')
            # On Long & Profit
            if price >= cost_basis * 1.06 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Profit')  
                del context.stocks_held[security]  
            # On Short & Profit
            if price <= cost_basis* 0.93 and current_position < 0:
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Profit')  
                del context.stocks_held[security]
            # On Long & Loss
            if price <= cost_basis * 0.97 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Loss')  
                del context.stocks_held[security]  
            # On Short & Loss
            if price >= cost_basis * 1.04 and current_position < 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Loss')  
                del context.stocks_held[security]
There was a runtime error.

Hi Koon,

I do appreciate the request. In order to run that backtest you're able to subscribe to the premium sets during that time period. Or you may try and collaborate with other folks on this thread.

Seong

Hi Seong,

Alright, thanks.

Thanks Seong, I actually didn't connect the HBI discussion to that line in code and must have taken it out because I didn't understand it. I've placed it back in. Here's a new algorithm with the following changes

-This algorithm now takes advantage of Accerns impact score
-Stocks with low impact score (lower than 20) are filtered out
-Weights are now determined by impact store
-Also I lowered the amount of downtime investment from 100% to 95% to match the long equity investment.

At this point I'm happy with where the algorithm is at so now I think it might be best to focus on the what the algorithm does when it's not actively trading the strategy. Currently it puts it all in SPY but this seems like a good opportunity to insert a different trading strategy!

Clone Algorithm
212
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
"""
PEAD (Post Earnings Announcement Drift)
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

Relevant Studies:
http://people.brandeis.edu/~heidifox/ese.pdf
and
http://papers.ssrn.com/sol3/papers.cfm?abstract_id=1783049
"""

import numpy as np

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, AverageDollarVolume


# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.estimize import (
    ConsensusEstimizeEPS,
    ConsensusWallstreetEPS,
    ConsensusEstimizeRevenue, 
    ConsensusWallstreetRevenue
)


# https://www.quantopian.com/data/eventvestor/earnings_calendar
from quantopian.pipeline.data.eventvestor import EarningsCalendar
from quantopian.pipeline.factors.eventvestor import (
    BusinessDaysUntilNextEarnings,
    BusinessDaysSincePreviousEarnings
)

# https://www.quantopian.com/data/accern/alphaone
from quantopian.pipeline.data.accern import alphaone

class PercentSurprise(CustomFactor):
    window_length = 1
    inputs = [ConsensusEstimizeEPS.previous_actual_value,
              ConsensusEstimizeEPS.previous_mean]

    def compute(self, today, assets, out, actual_eps, estimize_eps):
        out[:] = (actual_eps[-1] - estimize_eps[-1])/abs((estimize_eps[-1] + 0))
        
class DaysSinceRelease(CustomFactor):
    # Only getting the previous quarter's estimize surprise
    window_length = 1
    inputs = [EarningsCalendar.previous_announcement,
              ConsensusEstimizeEPS.previous_release_date]
    
    def compute(self, today, assets, out,
                earnings_announcement, estimize_release):
        days = estimize_release - earnings_announcement
        out[:] = abs(days.astype('timedelta64[D]').astype(int))

def make_pipeline(context):
    # Create our pipeline  
    pipe = Pipeline()
    
    # Instantiating our factors  
    factor = PercentSurprise()
    
    # Get the days since estimize release date and the latest
    # Earnings release date
    days_since_release = DaysSinceRelease()
    previous_estimize_release_date = \
        ConsensusEstimizeEPS.previous_release_date.latest

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Filter down to stocks in the top/bottom according to
    # the earnings surprise
    longs = (factor >= context.min_positive_surprise) & (factor <= context.max_positive_surprise)
    shorts = (factor <= context.min_negative_surprise) & (factor >= context.max_negative_surprise)

    # Set our pipeline screens  
    # Filter down stocks using sentiment  
    article_sentiment = alphaone.article_sentiment.latest
    impact_score = alphaone.impact_score.latest
    top_universe = is_liquid & longs & article_sentiment.notnan() \
        & (article_sentiment > .45) & impact_score.notnan() & (impact_score > 20)
    bottom_universe = is_liquid & shorts & article_sentiment.notnan() \
        & (article_sentiment < -.45) & impact_score.notnan() & (impact_score > 20)

    # Add long/shorts to the pipeline
    pipe.add(impact_score, "impactscore")
    pipe.add(top_universe, "longs")
    pipe.add(bottom_universe, "shorts")
    pipe.add(BusinessDaysSincePreviousEarnings(), 'pe')
    pipe.set_screen((days_since_release <= 7) &
                    (previous_estimize_release_date.notnull()))
    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))

    #Setup our hedge account
    context.hedgeAccount = symbol('SPY')

    #: Declaring the days to hold, change this to what you want
    context.days_to_hold_short = 2
    context.days_to_hold_long = 3
    #: Declares which stocks we currently held and how many days we've held them dict[stock:days_held]
    context.stocks_held = {}
    context.weights = {}

    #: Declares the minimum magnitude of percent surprise
    #: Negative surprise Delta must be a negative float
    #: Positive surprise Delta must be a positive float
    context.min_negative_surprise = -0.5
    context.max_negative_surprise = -1.0
    context.min_positive_surprise = 0.00
    context.max_positive_surprise = 0.055
    
    context.volume_limit = 0.01
    
    #: Weightage on shorts/long 
    #: MUST BE FLOAT
    #: Short weight must be negative
    context.long_weight = 0.95 #5% cash
    context.short_weight = -0.25
    context.hedge_weight = 0.95
    
    # 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_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())
    
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 
    context.results = pipeline_output('estimize')
    context.results = context.results[context.results['pe'] == 1]
    context.results = context.results.sort(['impactscore'], ascending=False)
    assets_in_universe = context.results.index
    context.positive_surprise = assets_in_universe[context.results.longs]
    context.negative_surprise = assets_in_universe[context.results.shorts]
    
    for security in context.positive_surprise:
        context.weights[security] = context.results.loc[security, "impactscore"]
    for security in context.negative_surprise:
        context.weights[security] = context.results.loc[security, "impactscore"]    
        

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        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
    record(leverage=context.account.leverage)

    # Check our positions for loss or profit and exit if necessary
    check_positions_for_loss_or_profit(context, data)  
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1
                share = port[security].amount
                if (context.stocks_held[security] > context.days_to_hold_long) and (share > 0):  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]
                    del context.weights[security]            
                elif (context.stocks_held[security] > context.days_to_hold_short) and (share < 0):  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]
                    del context.weights[security]    
                
    # Check our current 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.tolist() + current_negative_pos
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos
    
    if positive_stocks:
        order_target_percent(context.hedgeAccount, 0)
    elif not positive_stocks:
        order_target_percent(context.hedgeAccount, context.hedge_weight)
    
    #calculate weights
    pos_weight_total = 0.0
    neg_weight_total = 0.0
    for security in positive_stocks:
        pos_weight_total += context.weights[security]
    for security in negative_stocks:
        neg_weight_total += context.weights[security]
    
    # Rebalance our negative surprise securities (existing + new)
    for security in negative_stocks:
        price = data.current(security, 'price')
        volume_hist = data.history(security, 'volume', bar_count=30, frequency='1d')
        volume = volume_hist.mean()
        can_trade = (context.stocks_held.get(security) <= context.days_to_hold_short) or (context.stocks_held.get(security) is None)
        if data.can_trade(security) and can_trade and security != sid(32497): #removing HBI is for backtesting only
            order_pct_neg = max((context.short_weight * context.weights[security]) / neg_weight_total, -1.0)
            can_buy = (volume * context.volume_limit) > (context.portfolio.portfolio_value * order_pct_neg)/price
            if can_buy:
                order_target_percent(security, order_pct_neg)
                if context.stocks_held.get(security) is None:
                    context.stocks_held[security] = 0
            elif not can_buy:
                order_pct_neg = (volume * context.volume_limit * price)/context.portfolio.portfolio_value
                order_target_percent(security, order_pct_neg)
                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:
        price = data.current(security, 'price')
        volume_hist = data.history(security, 'volume', bar_count=30, frequency='1d')
        volume = volume_hist.mean()
        can_trade = (context.stocks_held.get(security) <= context.days_to_hold_long) or (context.stocks_held.get(security) is None)
        if data.can_trade(security) and can_trade and security != sid(32497): #removing HBI is for backtesting only
            order_pct_pos = min((context.long_weight * context.weights[security]) / pos_weight_total, 1.0)
            can_buy = (volume * context.volume_limit) > (context.portfolio.portfolio_value * order_pct_pos)/price
            if can_buy:
                order_target_percent(security, order_pct_pos)
                if context.stocks_held.get(security) is None:
                    context.stocks_held[security] = 0
            elif not can_buy:
                order_pct_pos = (volume * context.volume_limit * price)/context.portfolio.portfolio_value
                order_target_percent(security, order_pct_pos)
                if context.stocks_held.get(security) is None:
                    context.stocks_held[security] = 0
    
def check_positions_for_loss_or_profit(context, data):
    # Sell our positions on longs/shorts for profit or loss
    for security in context.portfolio.positions:
        is_stock_held = context.stocks_held.get(security) >= 0
        if data.can_trade(security) and is_stock_held and not get_open_orders(security):
            current_position = context.portfolio.positions[security].amount  
            cost_basis = context.portfolio.positions[security].cost_basis  
            price = data.current(security, 'price')
            # On Long & Profit
            if price >= cost_basis * 1.06 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Profit')  
                del context.stocks_held[security]
                del context.weights[security]        
            # On Short & Profit
            if price <= cost_basis* 0.93 and current_position < 0:
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Profit')  
                del context.stocks_held[security]
                del context.weights[security]    
            # On Long & Loss
            if price <= cost_basis * 0.97 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Loss')  
                del context.stocks_held[security]
                del context.weights[security]    
            # On Short & Loss
            if price >= cost_basis * 1.04 and current_position < 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Loss')  
                del context.stocks_held[security]
                del context.weights[security]    
There was a runtime error.

Nice work Lyth!

That may be a helpful way to take your research as it seems that for the past few days very few earnings announcements have met the filter criteria.

Quoting John Harper on this thread: https://www.quantopian.com/posts/news-and-blog-sentiment-pipeline-factors-with-accern#5759ec3b700321e3df000a47

http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2792559#%23

Abstract:

This paper uses a dataset of more than 900,000 news stories to test whether news can predict stock returns. We measure sentiment with a proprietary Thomson-Reuters neural network. We find that daily news predicts stock returns for only 1 to 2 days, confirming previous research. Weekly news, however, predicts stock returns for one quarter. Positive news stories increase stock returns quickly, but negative stories have a long delayed reaction. Much of the delayed response to news occurs around the subsequent earnings announcement.  

Hi Seong,
Is there a way to act on Earnings Reports the next market open after the information is available and not the market day after?
Setting pe to zero or adding an ne (for next earnings) and setting it to zero seems to not return any results in the algorithm.

Lyth, that's a great question and I hope to do my best in answering it.

I generally wouldn't advice on trying to act on Earnings Reports on the market open because of one main reason:

  • At the moment, you do not have an accurate way of knowing whether or not an earnings announcement was before or after market open. In the case that it was after market open and your logic is testing for nextearningsannouncement == 0, you'll essentially miss all earnings reports that fall after 4:00PM.
  • Because pipeline provides data on a daily basis, Estimize's consensus estimates and Accern's alphaone are both getting yesterday's data, today. This is why your algorithm is not making any trades, because the data present on t==0 aren't meeting the necessary conditions for the algorithm to trade.

Hope that helps and I'm happy to help answer any questions

Seong

Also as an observation, I've been live trading this example strategy for a few weeks now and it seems that in the past 30~ days, it hasn't made any trades. From what I know it could be for a few reasons:

  • The trading signal triggers are not being triggered by the earnings announcements (perhaps the percent limits are too strict)
  • Estimize's data coverage as compared to someone like Zack's is a lot smaller, so the mix of small coverage and the current time of year contribute to the lack of triggered trades

Overall, the backtested strategy had relatively few trades and I would suggest more work is needed to make it robust enough to be holding positions consistently as opposed to all cash (similar to what Lyth has worked on).

With analyst earnings surprises from Zacks available soon in pipeline and estimates to follow, it'll be interesting to see how the two will compare.

Has anyone modified the strategy to use Zacks yet?

We've recently found an issue with the Estimize Consensus Estimates dataset in how updates are being made. While we're still investigating the problem, what we know so far is that updates to the dataset are not properly being delivered to algorithms. This appears to have been happening since the beginning of June, 2016.

To prevent algorithms from using this dataset until we've addressed and implemented the solution, we're shutting down access to the Estimize Consensus Estimate starting Monday, July 18th.

This algorithm has instead been modified to use analyst earnings surprises from Zacks

Clone Algorithm
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
import numpy as np

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, AverageDollarVolume

from quantopian.pipeline.data.zacks import EarningsSurprises

# 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
)

# from quantopian.pipeline.data.accern import alphaone_free as alphaone
# Premium version availabe at
# https://www.quantopian.com/data/accern/alphaone
from quantopian.pipeline.data.accern import alphaone as alphaone

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

    # Instantiating our factors  
    factor = EarningsSurprises.eps_pct_diff_surp.latest

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Filter down to stocks in the top/bottom according to
    # the earnings surprise
    longs = (factor >= context.min_surprise) & (factor <= context.max_surprise)
    shorts = (factor <= -context.min_surprise) & (factor >= -context.max_surprise)

    # Set our pipeline screens  
    # Filter down stocks using sentiment  
    article_sentiment = alphaone.article_sentiment.latest
    top_universe = is_liquid & longs & article_sentiment.notnan() \
        & (article_sentiment > .30)
    bottom_universe = is_liquid & shorts & article_sentiment.notnan() \
        & (article_sentiment < -.30)

    # Add long/shorts to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(bottom_universe, "shorts")
    pipe.add(BusinessDaysSincePreviousEarnings(), 'pe')
    pipe.set_screen(factor.notnan())
    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 = 4
    #: 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 = .06

    #: 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_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())

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]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]
    context.negative_surprise = assets_in_universe[results.shorts]

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        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
    record(leverage=context.account.leverage)

    # Check our positions for loss or profit and exit if necessary
    check_positions_for_loss_or_profit(context, data)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1  
                if context.stocks_held[security] >= context.days_to_hold:  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]  
            # If we've deleted it but it still hasn't been exited. Try exiting again  
            else:  
                log.info("Haven't yet exited %s, ordering again" % security.symbol)  
                order_target_percent(security, 0)  

    # Check our current 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.tolist() + current_negative_pos
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos
    
    # Rebalance our negative surprise securities (existing + new)
    for security in negative_stocks:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            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:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            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.current(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 check_positions_for_loss_or_profit(context, data):
    # Sell our positions on longs/shorts for profit or loss
    for security in context.portfolio.positions:
        is_stock_held = context.stocks_held.get(security) >= 0
        if data.can_trade(security) and is_stock_held and not get_open_orders(security):
            current_position = context.portfolio.positions[security].amount  
            cost_basis = context.portfolio.positions[security].cost_basis  
            price = data.current(security, 'price')
            # On Long & Profit
            if price >= cost_basis * 1.06 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Profit')  
                del context.stocks_held[security]  
            # On Short & Profit
            if price <= cost_basis* 0.94 and current_position < 0:
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Profit')  
                del context.stocks_held[security]
            # On Long & Loss
            if price <= cost_basis * 0.94 and current_position > 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Long for Loss')  
                del context.stocks_held[security]  
            # On Short & Loss
            if price >= cost_basis * 1.06 and current_position < 0:  
                order_target_percent(security, 0)  
                log.info( str(security) + ' Sold Short for Loss')  
                del context.stocks_held[security]  
There was a runtime error.

Access to the Estimize dataset will temporarily be shut down starting today.

We've identified an issue with the Estimize dataset that prevented updates to the data starting June, 2016. All subscribers have been notified and we are taking steps to implement a solution.

For an alternative version using Wall Street Consensus Estimates, please view this thread: https://www.quantopian.com/posts/updated-long-slash-short-earnings-sentiment-trading-strategy-with-the-streets-consesus

Hi, Seong
Thanks for your strategy!
Could you recommend me some articles about how you get the surprise limitation ,the profit limitation and sentiment limitation?

Karen

Hi Seong,

I do not quite understand why you set a maximum for surprise magnitude. After all, won't greater surprise generate greater momentum? Say in Case 1 the actual beats the estimate by 10% and in Case 2 the actual beats the estimate by 5%. I would expect more upward price movement in Case 1 than in Case 2. Why set a maximum to NOT trade Case 2? What is the rationale for setting the surprise range? Thanks!

Neo