By Cheng Peng
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.
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.
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
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),
)
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)
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)
factor = (EarningsSurprises.eps_pct_diff_surp.latest.rank() + Returns(window_length=5).rank())
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)
factor = (EarningsSurprises.eps_pct_diff_surp.latest.rank() + Returns(window_length=5).rank())
filter_factor = EarningsSurprises.eps_std_dev_est.latest
point_in_time = BusinessDaysUntilNextEarnings().eq(1)
universe = QTradableStocksUS() & factor.notnan() & filter_factor.notnan()
universe = filter_factor.rank(mask=universe).percentile_between(70, 100) & point_in_time
run_new_event_study(
universe=universe,
factor=factor,
days_before=0, days_after=5,
long_short=True,
bins=None, quantiles=4)
factor = Fundamentals.pe_ratio.latest
point_in_time = BusinessDaysSincePreviousEarnings().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)
factor = Fundamentals.pe_ratio.latest
filter_factor = (EarningsSurprises.eps_pct_diff_surp.latest**2)**0.5
point_in_time = BusinessDaysSincePreviousEarnings().eq(1)
universe = QTradableStocksUS() & factor.notnan() & filter_factor.notnan()
universe = filter_factor.rank(mask=universe).percentile_between(0, 30) & point_in_time
run_new_event_study(
universe=universe,
factor=factor,
days_before=0, days_after=5,
long_short=True,
bins=None, quantiles=4)
This notebook is for informational purposes only and does not constitute as investment advice in any security or any form. There are no guarantees for accuracy or completeness, and all information is subject to the reader's interpretation.