Quantopian's community platform is shutting down. Please read this post for more information and download your code.
Back to Community
Quantpedia Trading Strategy Series: Reversals in the PEAD

In his white paper "Overreacting to a History of Underreaction", Milian explores the possibility that well known cross sectional anomalies can reverse over time. Specifically, he investigates the reversal of the PEAD effect. He finds that contrary to previous research, stocks with the most negative previous earnings surprise actually exhibit the most positive returns following the subsequent earnings announcement.

This can be attributed to the idea that investors know they typically underreact to earnings announcement news (the underlying explanation for the PEAD). Thus, by compensating for their underreaction, they overreact to substantial surprises in earnings announcement news, positioning themselves in alignment with the expectation of the PEAD effect. When the next earnings announcement comes, the overcrowding of investors pushes the market beyond efficient, resulting in the correction of investor sentiment and a negative correlation for firm's earnings news in the following days.

Quantpedia summarizes the article's possible explanation for the PEAD reversal:

The paper speculates that it seems that due to their well-documented history of apparently underreacting to earnings news, investors are now overreacting to earnings announcement news. However classical PEAD (post-earnings announcement drift) literature examines mainly quarterly portfolio returns while this academic paper focuses on 2-days retun therefore it is probable that PEAD still holds and both anomalies exists concurrently.

OOS Study Results
I conducted a similar study examining the reversal in the PEAD, over a sample period from 2011 - 2016 (compared to the Milian's 2003 - 2010). Overall, I found my results to be consistent with Milian's results, when considering a hold period of 9 days following the Earnings Announcement, rather than 2.

I found that firms in the highest decile of past earnings surprise underperform stocks in the lowest decile by -1.78% over a hold period of 10 days (compared to -1.59% over a hold period of 3 days found in the paper).

Trading Strategy Details
1. Each day, pick stocks in the Q500US which have Earnings Announcements the next day
2. Go short on stocks in the highest decile of previous earnings surprise, long on stocks in the lowest decile of previous earnings surprise
3. Hold for a period of 10 days, then close the position

N.B.: As a result of parameter optimization, this strategy may be overfit.

Attached is the whitepaper walkthrough and OOS validation of the original study (Hit "Clone Notebook" to see a complete analysis). The backtest and backtest analysis are attached to the thread below.

FAQ

What is the Quantpedia Trading Strategy Series?

Quantpedia is an online resource for discovering trading strategies and we’ve teamed up with them to bring you interactive and high quality trading strategy examples based off financial research. Our goal is that you’re able to replicate the process we’ve used here for your own research and backtesting.

Where can I find more trading strategy ideas?

You can find the full Quantpedia Series here along with other research. Other than that, you can browse Quantpedia’s strategies or look through our forums for ideas posted by community members. Want to feature your own? Submit your proposal to SLEE @ quantopian.com

I can only run the backtest till 2014, why is that?

This algorithm uses EventVestor's Earnings Calendar and Zack's Earnings Surprises dataset to time earnings announcements and measure earnings surprises, which are both premium datasets.

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.

38 responses

PEAD Reversal Rolling Rebalance

Logic:
1. Each day, run a pipeline for stocks in the Q500, selecting the top decile of previous earnings surprise (shorts), and lowest decile of previous earnings surprise (longs)
2. Increment the hold times for each stock held in the portfolio, only keeping stocks for which the hold time < context.days_to_hold
3. Open new long/short positions for stocks generated by the pipeline, equally distributing the portfolio between all long/shorts currently held (70/30)
4. Close positions for stocks which have been held for > context.days_to_hold

Summary:
This algorithm is an implementation of the above strategy, leveraging the PEAD reversal found after earnings announcements
Note that this algorithm uses the LagESurp indicator generated by the Zacks Earnings Surprise dataset, rather than the stronger LagEaRet indicator

EDIT: This backtest was generated using the premium portion of the EventVestor's Earnings Calendar and Zack's Earnings Surprises datasets. To clone a backtest generated using the free portion of these datasets, click here.

Pyfolio tearsheet of above algorithm

I cloned this algo. As I run the back testing, I got error. Seems I have to buy the datas?

Thomas.

Hi Thomas,

This algorithm uses the EventVestor Earnings Calendar and Zack's Earnings Surprise data sets which are free from 27 May 2006 - 30 Jul 2016 to 01 Jan 2007 - 28 Oct 2014 respectively. If you wanted to run this backtest yourself, you should run it within those dates.

Ok, this means 'no money no talk'. :-)

Many thanks!

Hi Matthew

Many thanks for this, very interesting stuff.
I cloned your Notebook and tried to run but i get a whole bunch of errors as per screenshot

http://i.imgur.com/xwtE7i5.png

Would you know what the issue is?

Thnx

Hi Matthew,

Any news on the above errors?

Has anyone been able to run Matthew's notebook? I get errors as per above.

Thanks

Hi Photi,

Can you post the uncropped error message which you are receiving? We are aware of an issue with the Earnings Calendar dataset, and we're working quickly on a solution.

Thanks
Matt

This is the full error I receive.

<error removed>  

Yup i get same error.

I also get an error. I spot checked the error above against mine and it appears to be slightly different, so I'm including it in a post here.
So when running this:

start = pd.Timestamp("1-05-2011")  
end = pd.Timestamp("09-25-2016")  
stock_data = create_data(Q500US(), start, end, 12)  

I'm getting the following error, which is isolated to the create_data method:

<error removed>  

Hi all,

Thanks for the comments. The error is a result of a bug with the earnings calendar dataset, and our team is quickly working on the solution. Once the fix is shipped the code in the notebook/algorithm should run fine.

Matt

Hey guys,

The problem with the earnings calendar dataset has been fixed. You should now be able to run the notebooks/backtests using earnings calendar data. Sorry for the trouble.

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.

Hello Matt,

Would you please explain about why writing 'context.MAX_IN_ONE' in the 'order_target_percent()' function in line 79 and 84 of the code ?
Thanks in advance:)

Hey Matt,

Thanks for posting your analysis -- very cool.

Can you please explain why you started your analysis & backtesting in 2011 and didn't go back to e.g. 2008? Also, did you experiment with various long/short ratios at all?

Thanks,
Takis

Ruocong,

Currently, the context.MAX_IN_ONE variable is not utilized (since its value is 1). This variable is modifiable so you can set a threshold of the max % of your portfolio to be allocated in one stock.

Takis,

I was interested in doing a purely out of sample test. Since the original paper covered 2003-2010, I chose to start my tests in 2011. However, you can easily modify the algorithm/notebook to test back to 2008.

I found that most of my returns were stemming from the long portion of the strategy (hence the 70/30 split). The strategy remains profitable at a 50/50 long short split as well.

I wonder if this algorithm is significantly overfitted. I ran the algo on Q1500 from 10/6/2013 to 10/26/2014 to capture approximately 4 earnings seasons. The results were less than spectacular.

Sofyan Saputra - The issue with Q1500 is there are 1500 stocks vs Q500 500 stocks (higher caps, less volatility). Q1500 puts have too many positions, and have too much volatility.

Matthew- Thanks for building the algo. It's very good quality and easy to understand. I made few minor tweaks. For one , I found 4 days did much better than 6 days. (what are your thoughts on this) . I also ended subscribing to the full dataset $25 - good price to me.

Sofyan,

To some extent, this strategy is overfit as I factored in past performance for picking the hold times and long/short ratio for the strategy. The original paper suggested trading stocks with active options trading - I used the Q500 as a proxy since there is currently no options data available on Quantopian. Thus, I can't speak for the validity of the strategy for different universes of stock selections.

Quoc,

Thanks! A hold time of 4 days for backtesting could definitely perform better than a 6 day hold strategy. Note that in my notebook, I picked my hold time based off cumulative average returns for each stock (not overall returns for the strategy). So, it's very possible that a 4 day hold period leads to higher backtest results. Imagine the scenario where you are able to purchase a larger portfolio % of a high returning stock S on a day x + 4, because you sold other stocks on day x. If your hold time were 6 days, you would not be able to purchase as much stock S, and your overall returns would be lower.

Matthew,

This is a great example and I'm working through the code to understand some of the details.

One place of confusion for me is in create_deciles_pipeline() which calculated 'LagEaSurp Decile' and later in create_data() where 'LagEaSurp Decile' is again calculated. Is there a problem with calculating the decile in the create_deciles_pipeline() code or is this due to so some other factor?

It appears that there are two data sets, stock_data and decile_data that may or may not contain the same securities. Can you comment on this?

Thanks,
Justin

Hey Matt,

Thanks for the response. I've been spending a good amount of time looking at methods to exploit trends in earnings reports. This strategy seems to have produced some of the best results I have seen so far.

However, in an actual brokerage, it's costly to do re-balancing frequently because doing so would rack up a lot of slippage and commissions. In your algo, we are rebalancing for each of the many stocks during the course of their 6 day lifespans. So I was looking to do a version of this algorithm which holds the stocks the day before the earnings (2 hours before market close of earnings day) and then exits all of the stocks before opening the next set of new positions (3 hours before market close). This allows us to have the capital to constantly trade earnings without rebalancing a bunch of shares every day. The holding time is only one day, but I assumed this would be enough to capture the earnings move. However, the results are nowhere near as good, even if I use the Q500. See attached backtest.

As a result, I am wondering whether the daily rebalancing is essentially causing a Martingale effect. Whenever the algo incorrectly picks the earnings, the rebalance will add to the position to keep the ratio. So in the course of 6 days, adding to a position repeatedly as it's losing, decreases the cost basis. Since the algo picks significantly more longs, the fact that we were in a bull market means martingaling when a stock is going down will be extremely profitable.

So a few things:
1. Is the algo benefiting heavily from the bull market environment that we have been in and may not hold in the future? Is this martingale-like effect, the reason why a 1 day hold without a rebalance the next day and a wider array of stocks (Q1500) that weren't as bullish as the large cap are significantly less effective?
2. Is there some other way to effectively utilize this strategy without doing more than an opening and closing trade per stock per earnings report? I'm interested in testing it out in a real environment or at least IB paper to see if it actually works. However, for a reasonable amount of money invested in this strategy (e.g. 30K), repeatedly trading one or two shares of many individual stock to do a rebalance, doesn't really make sense.
3. 500% in several years is absolutely insane. I apologize for the skepticism, but if this strategy is real and consistent, wouldn't everyone be clamoring to live trade it or is there some caveat that makes this much less effective in real life?

Justin,

Great question. The problem with computing the deciles purely in pipeline is that the deciles are recomputed every day. Thus, you have a rolling decile computation (which is not what we want). Instead, I use the decile data computed on the first day of each quarter, and the create_data function computes the decile for each quarter in our time period so our deciles our constant throughout each quarter.

In the actual algorithm, I simplify things by just using the pipeline decile computation at each month. In the notebook however, I try to match the original paper's approach as accurately as possible.

Sofyan,

It's my intuition that the viability of exiting positions after one day of holding is heavily influenced by the outcome of the actual earnings announcement. In this strategy, we don't care about the result of the upcoming earnings report (we just happen to know that the PEAD effect is most pronounced during the days following the subsequent earnings announcement, so the reversal will yield the most returns in this period). So, exiting trades on the day/day after of the earnings report is likely a substantially different strategy.

I would recommend checking out the holding time variations graph in my notebook to see my findings on holding periods.If you wanted to make a more market neutral strategy, you could balance the long/shorts at 50/50.

If you wanted to forego the rebalancing, I'd recommend sticking to a hold period of more than a couple days, but tweaking the algorithm to maintain a constant position in all stocks it enters (up to a certain cap).

The key here is that past returns are not indicative of future performance. Clearly, this strategy has performed well in the past couple years. In order to make it a real trading strategy, there is much more analysis to be done and edge cases to handle for actual live trading.

Also - for anyone interested, here are the results of the backtest for a 2 day hold period

I created a version of the algo which forgoes the rebalancing. It has a holding period of two days and it determines the amount per trade by dividing the amount of capital by the number of total earnings that meet our criteria in the next two business days. It will never put more than 20% of the portfolio into a single trade. From the leverage variable plotted, the method seems to do a good job of using a leverage of approximately 1 when there are a lot of earnings reports and less leverage with fewer earnings reports. However, I can't seem to replicate the insane results without doing the rebalancing.

I think a reason for this might be because with the portfolio allotment method, you are going all-in on a single stock when there are no earnings reports. This is visible from the position variable I plotted, which shows for certain weeks there is only 1 company that meet our requirements. A version of the algo without the rebalancing won't benefit from these all-ins, which seems to have been very profitable in the past, but risky going forward.

What's unusual is that the results are not a clear upward profit chart like yours despite having the exact same methodology for entering stocks. Did I make a silly mistake in how I implemented the algo without rebalancing or is there no way to replicate the success of the original algo without the rebalancing and going all in on a single stock during earnings off-season?

First post around here :) So you're fully Amateur warned...This is a very interesting community, glad I found it.

Sofyan, I saw your concerns, so I created some custom data to follow Matt's original algorithm, and also limited MAX_IN_ONE to 0.2. Using the free test period, this tweaked algorithm (still with 70% long, 30% short, 6 day hold) results in 117% return vs Matt's more risky 100% allowed in one stock which returned 380% for the same time frame (backtest results not shown).

In Matt's rebalancing approach with max hold of 2 days, effectively if you started one day with one stock, and the next day expanded into two, you would have 1/2 position owned for one day, 1/2 position for 2 days. This seems to agree with Matt's results that 2 days provided better returns than 1 day. In theory (assuming correct implementation), a hold time of 1 day on the originally coded algorithm has effectively no rebalancing. This provides a return of 154% compared to Matt's original of 380% for the same time frame. This is the backtest shown below.

Even for the 1 year of testing you did above, tuned to 1 day of hold and 20% max per security performs better than your results (returning 92% vs SPY 22%). This would likely suggest a bug or two somewhere in your implementation.

Thanks for the insights Jeff.

The reason, my one day results are different is because immediately after the earnings report, I drop my positions and reopen new ones at market open. This is visible in my schedule functions. Whereas with the original algorithm. if you look at the transaction history, even if "context.DAYS_TO_HOLD = 1", you see multiday transactions. Check out the transaction history of the DAYS_TO_HOLD = 1, you will notice stocks are being held and rebalanced for a whole day before it is let go 2 business days later. It appears DAYS_TO_HOLD needs to be 0 to truly match just holding through the earnings report without rebalancing.

The results are closer when DAYS_TO_HOLD = 0, but I'm also not setting a limit or ratio to the number of longs or shorts I want in my portfolio; instead, I position equally in all positions that meet the requirement. So if there are 10 shorts and 2 longs that meet the LagESurp top/bottom decile requirement, I will invest equally into each stock based off my available capital without regard for portfolio long/short ratio.

Additionally, what I'm seeing with the original algo when it's at DAY_TO_HOLD > 0 and doing rebalancing, is that it is often changing a significant amount of shares to the position right after the earnings report. For example, see this AMT trade. Perhaps this combination of making sure your portfolio is at a specific ratio and adding to a position after the earnings report is what is multiplying the profits to this strategy.

Big props to Matthew for covering this strategy. Ernie Chan just picked it up on his blog a few weeks after Matt did.

http://epchan.blogspot.com/2016/11/pre-earnings-annoucement-strategies.html

Did something break with this? backtest doesn't work for current dates, and performance seems off.

Hi Elsid,

Have you subscribed for the premium version of EventVestor Earnings Calendar dataset? The free version only runs up until a year ago.

Ohh no I am not, also I ran this backtest further out from 07, and it get's completely crushed with like 75% DD what's the point of this algo? Or is this due to not having premium data?

Hi Everyone,

I cloned the original algorithm and changed it to utilize the recently released Optimize API. Let me know what you think.

This algorithm was updated to make use of the recently introduced Optimize API. The trading strategy remained the same. This iteration altered the method that was used to place orders and construct our portfolio. Portfolio construction was done using the order_optimal_portfolio function. We passed in an objective (a dictionary mapping securities to desired weights in the portfolio), and a universe of stocks we were interested in (an iterable of Equity objects). Something I noted is that when using a leverage constraint in order_optimal_portfolio, I had to surround the function with a try except block. This was to avoid a SolverError from the cvxpy module (a module utilized in order_optimal_portfolio). The error only occurred when using a leverage constraint. I'd love to dig into the bug with the help of the community.

-Jeremy

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.

This is the same version as Jeremy's, but backtested over the free portion of the EventVestor's Earnings Calendar and Zack's Earnings Surprises datasets.

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.

Here's the algo with:

    objective = opt.TargetPortfolioWeights(weights)  
    constraints = []

    constraints.append(opt.MaxGrossLeverage(MAX_GROSS_LEVERAGE))  
    constraints.append(opt.DollarNeutral())  
    algo.order_optimal_portfolio(  
        objective=objective,  
        constraints=constraints,  
        universe=universe  
    )  

Gets it more in line with the contest/Q fund requirements.

It'd be nice if someone were to express the factor in a standard way, so that it could be added to a multi-factor algo (e.g. see https://www.quantopian.com/posts/quantcon-nyc-2017-advanced-workshop or https://www.quantopian.com/posts/long-short-multi-equity-algo). Feasible?

so if LagESurp is the lagged surprise, in real-time trading, how could we get the dataset?

As I ran the Ernesto's update version, the RETURN I got is much lower (see my backtesting). Why?

https://www.quantopian.com/posts/quantpedia-trading-strategy-series-reversals-in-the-pead#594036bcd7315c0010357ada

I wonder if someone has subscribed the datasets and test it for the recent yreas (tiil current)?

Doesn't these algos suffer from Look-Ahead Bias? As far as I understood from them they buy/short the day before of the surprise, but surprises are announced the same day as the earnings announcement, early in the morning, but still the same day anyways.

Great work , yet am still confused as to where you have indicated - percentage wise - out of all securities tested , as how many were subject to such effect , and how many did not experience reversal ? and are there any clustering effects in terms of what type of securities that have experienced such effect versus those who did not ?

Your inputs are highly appreciated
Best Regards