Notebook

Behavioral Arbitrage - Design Strategies That Time Market Mistakes

By Cheng Peng

Video Here

Behavioral Arbitrage refers to arbitrage opportunities that are based on how human biases affect the capital markets, especailly prices - these opportunities are typically known as the Behavioral Gap. There are multiple ways human behavior can influence market prices, and so, awareness of these behaviors can help in our analysis and trading. Generally, there are two types of biases: cognitive and emotional. Cognitive biases are generally hard to notice because they appear when interpreting and processing information. Emotional biases are easier to recognize - but difficult to manage since our biological responses can upset even the most rational plans.

Proposed Hypothesis Framework

  1. What is the market thinking?
    • What is the current sentiment of the market towards this asset? Is it bullish, bearish or going unnoticed?
  2. Why is the market wrong?
    • Based on current information, is the market sentiment correct? Too extreme or just wrong?
  3. How wrong is the market?
    • What conditions can further explain the difference between sentiment and reality?

Event Studies

This framework is generally enough to be applied to any model - however, I find event studies to be particularly fitting for behavioral explanations. Event studies bring homogeneity to the data by filtering and isolating most of the noisy information irrelevant to our samples. This notebook will focus on Earnings Announcements in stocks, and especially how new information directly impacts future market prices.

First, we create a few convenient methods for running event studies with a factor.

In [1]:
import alphalens
from scipy import stats

from quantopian.pipeline import Pipeline
from quantopian.research import run_pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import Returns
from quantopian.pipeline.experimental import QTradableStocksUS
from quantopian.pipeline.data import Fundamentals
from quantopian.pipeline.factors.eventvestor import BusinessDaysUntilNextEarnings, BusinessDaysSincePreviousEarnings

# Earnings Imports Zacks
from quantopian.pipeline.data.zacks import EarningsSurprises
from quantopian.pipeline.factors.zacks import BusinessDaysSinceEarningsSurprisesAnnouncement
from quantopian.pipeline.factors.eventvestor import BusinessDaysUntilNextEarnings, BusinessDaysSincePreviousEarnings
In [2]:
start = '%s-01-01' % 2011
end =  '%s-12-31' % 2015

def make_pipeline(factor, universe):
    combined_alpha = factor.zscore(mask=universe)
    
    pipe = Pipeline(
        columns={
            'combined_alpha': combined_alpha
        },
        screen=universe & combined_alpha.notnan()
    )
    return pipe

def run_new_event_study(
    universe=QTradableStocksUS(), 
    factor=Returns(window_length=5), 
    days_before=5, days_after=5, 
    long_short=False,
    quantiles=None,
    bins=[0],
    periods=(1,2,3,4,5),
    price_type='open_price'):

    results = run_pipeline(make_pipeline(factor, universe), start_date=start, end_date=end)
    security_universe = results.index.levels[1].unique().tolist()

    prices = get_pricing(security_universe, start_date=start, end_date=end, fields=price_type)
    factor_data = alphalens.utils.get_clean_factor_and_forward_returns(
        factor=results['combined_alpha'], 
        prices=prices,
        periods=periods,
        quantiles=quantiles,
        bins=bins
    )

    alphalens.tears.create_event_study_tear_sheet(
        factor_data,
        prices,
        avgretplot=(days_before, days_after),
    )

Recency Biases Pre Earnings

  1. What is the market thinking?
    • Weekly returns + last earnings surprise
  2. Why is the market wrong?
    • Exaggeration based on last earnings surprise
  3. How wrong is the market?
    • Unstable earnings estimates lead to larger discrepancies

Event Studies To Run

  • Earnings Surprise Before Earnings
  • Weekly Returns Before Earnings
  • (Earnings Surprise + Weekly Returns) Before Earnings
  • (Earnings Surprise + Weekly Returns) Before Earnings Filtering For Unstable Estimates

Earnings Surprise Before Earnings

In [5]:
factor = EarningsSurprises.eps_pct_diff_surp.latest
point_in_time = BusinessDaysUntilNextEarnings().eq(1)
universe = QTradableStocksUS() & factor.notnan() & point_in_time
run_new_event_study(
    universe=universe, 
    factor=factor, 
    days_before=0, days_after=5, 
    long_short=True, 
    bins=None, quantiles=4)
Dropped 0.3% entries from factor data: 0.0% in forward returns computation and 0.3% in binning phase (set max_loss=0 to see potentially suppressed Exceptions).
max_loss is 35.0%, not exceeded: OK!
Quantiles Statistics
min max mean std count count %
factor_quantile
1.0 -12.455037 0.469154 -0.682293 1.039889 8475 28.259420
2.0 -0.974619 0.702058 -0.108819 0.175358 6833 22.784261
3.0 -0.564710 1.171512 0.024015 0.171587 7053 23.517839
4.0 -0.446910 12.364636 0.833095 1.205553 7629 25.438479
<matplotlib.figure.Figure at 0x7f009c084bd0>
<matplotlib.figure.Figure at 0x7f0096838390>

Weekly Returns Before Earnings

In [6]:
factor = Returns(window_length=5)
point_in_time = BusinessDaysUntilNextEarnings().eq(1)
universe = QTradableStocksUS() & factor.notnan() & point_in_time
run_new_event_study(
    universe=universe, 
    factor=factor, 
    days_before=0, days_after=5, 
    long_short=True, 
    bins=None, quantiles=4)
Dropped 0.0% entries from factor data: 0.0% in forward returns computation and 0.0% in binning phase (set max_loss=0 to see potentially suppressed Exceptions).
max_loss is 35.0%, not exceeded: OK!
Quantiles Statistics
min max mean std count count %
factor_quantile
1 -8.869759 0.432751 -1.160334 0.691155 8313 26.195878
2 -0.994887 0.683128 -0.231266 0.214357 7806 24.598223
3 -0.627731 1.196967 0.263298 0.208894 7526 23.715888
4 -0.196040 8.645558 1.171104 0.688884 8089 25.490011
<matplotlib.figure.Figure at 0x7f0096757590>
<matplotlib.figure.Figure at 0x7f0096757a90>