
“每天,投资者从纽约证券交易所(NYSE)、美国证券交易所(AMEX)和纳斯达克(NASDAQ)中选择即将发布财报的股票,目标是活跃期权市场的大型股票。他们根据过去的财报表现进行交易,持有两天,且投资组合中的权重相等。”
资产类别:股票 | 地区:美国 | 频率:每日 | 市场:股权 | 关键词:PEAD
策略概述
投资者关注纽约证券交易所(NYSE)、美国证券交易所(AMEX)和纳斯达克(NASDAQ)的股票,尤其是具有活跃期权市场的大型股票,每天选择那些宣布第二天公布财报的股票。他们根据股票在上一次财报发布期间的表现来评估股票。该策略涉及买入过去收益表现最差的十分位组合,并卖空那些过去表现最好的组合。头寸维持两天,且投资组合中的权重相等。这种方法旨在利用股票对财报公告的异常反应的预期逆转。
策略合理性
学术论文指出,投资者在历史上以对财报新闻反应不足而闻名,现在可能正在对这类公告过度反应。传统上,关于财报公告后走势(PEAD)的文献主要调查季度投资组合收益。相比之下,本文聚焦于两天内的收益。这种焦点上的差异提出了一个可能性,即PEAD仍然有效,财报新闻的欠反应和过度反应可以作为两种独立的现象共存。这意味着,尽管短期反应可能表现出过度反应,但长期的PEAD效应,即股价逐渐对财报新闻进行调整的特征,仍然可能存在。
论文来源
Overreacting to a History of Underreaction? [点击浏览原文]
- Jonathan A. Milian, 美国佛罗里达国际大学
<摘要>
先前的研究已经记录了企业财报公告新闻中长期正自相关的悠久历史。这是财报公告后走势现象的主要特征之一,通常被归因于投资者对财报新闻的不足反应。我记录到,对于具有活跃交易所交易期权的公司,这种自相关已经显著变为负值。对于这些容易套利的公司,在其下一个财报公告中,前财报公告异常收益率(前财报意外)排名最高的十分位公司平均表现比排名最低的公司低1.29%(0.73%)。额外的分析与投资者学习有关财报公告后走势并过度补偿的观点一致。由于他们对财报新闻显然表现出不足反应的历史有充分的记录,投资者现在似乎对财报公告新闻反应过度。本文显示,基于相对估值的流行交易策略的尝试可能会显著逆转先前记录的模式。

回测表现
| 年化收益率 | 40.32% |
| 波动率 | 66.29% |
| Beta | -0.032 |
| 夏普比率 | 0.58 |
| 索提诺比率 | 0.46 |
| 最大回撤 | -95.4% |
| 胜率 | 48% |
Python代码及解释
完整python代码
from quantlib import AssetData, TradingCostModel, TradingControl
from StrategyCore import *
import pandas as pd
from collections import defaultdict
from typing import Dict, Tuple
from pandas.tseries.offsets import BusinessDay
from dateutil.relativedelta import relativedelta
import json
from datetime import datetime, date
class EarningsReversalStrategy(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2009, 1, 1) # Start Date
self.SetCash(100000)
self.leverage = 5
self.earnings_lookback = 30
self.last_rebalance_year = -1
self.last_rebalance_month = -1
self.asset_info = {}
# Earnings and EPS data storage
self.earnings_announcement_data = defaultdict(list)
self.earnings_performance_data = defaultdict(lambda: defaultdict(dict))
self.initial_date = None
earnings_info = self.Download('data.quantpedia.com/backtesting_data/
economic/earnings_dates_eps.json')
earnings_info_parsed = json.loads(earnings_info)
for record in earnings_info_parsed:
announcement_date = datetime.strptime(record['date'], '%Y-%m-%d').date()
if not self.initial_date:
self.initial_date = announcement_date
for stock_info in record['stocks']:
symbol = stock_info['ticker']
self.earnings_announcement_data[announcement_date].append(symbol)
if stock_info['eps'].strip():
year, month = announcement_date.year, announcement_date.month
self.earnings_performance_data[year][month][symbol] =
float(stock_info['eps'])
self.recent_ear_values = []
self.past_ear_values = []
self.strategy_control = TradingControl(self, 10, 10, 2)
self.main_asset = self.AddEquity('SPY', Resolution.Daily).Symbol
self.is_selection_time = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.select_coarse, self.select_fine)
self.Schedule.On(self.DateRules.MonthStart(self.main_asset),
self.TimeRules.AfterMarketOpen(self.main_asset), lambda: self.is_selection_time = True)
def select_coarse(self, coarse):
for asset in coarse:
if asset.Symbol in self.asset_info:
self.asset_info[asset.Symbol].update_price(self.Time, asset.AdjustedPrice)
if not self.is_selection_time:
return Universe.Unchanged
self.is_selection_time = False
prev_month = (self.Time - relativedelta(months=1)).date()
self.last_rebalance_year, self.last_rebalance_month = prev_month.year, prev_month.month
eligible_assets = [x for x in coarse if x.Symbol.Value in
self.earnings_performance_data[self.last_rebalance_year][self.last_rebalance_month]]
for asset in eligible_assets:
if asset.Symbol not in self.asset_info:
self.asset_info[asset.Symbol] = AssetData(self.earnings_lookback)
historical_prices = self.History(asset.Symbol, self.earnings_lookback,
Resolution.Daily)
if not historical_prices.empty:
for timestamp, price in historical_prices.loc[asset.Symbol].close.items():
self.asset_info[asset.Symbol].update_price(timestamp, price)
return [asset.Symbol for asset in eligible_assets if self.asset_info[asset.Symbol].is_data_ready()]
def select_fine(self, fine):
eligible_symbols = []
for asset in fine:
symbol = asset.Symbol
if symbol.Value in self.earnings_performance_data[self.last_rebalance_year]
[self.last_rebalance_month]:
earnings_date = max(self.earnings_performance_data[self.last_rebalance_year][self.last_rebalance_month][symbol.Value].keys())
date_range_start = earnings_date - BusinessDay(2)
date_range_end = earnings_date + BusinessDay(1)
market_return = self.asset_info[self.main_asset].calculate_return(date_range_start, date_range_end)
asset_return = self.asset_info[symbol].calculate_return(date_range_start, date_range_end)
if market_return is not None and asset_return is not None:
ear = asset_return - market_return
self.earnings_announcement_data[symbol].append((earnings_date, ear))
self.recent_ear_values.append(ear)
eligible_symbols.append(symbol)
if not eligible_symbols:
return Universe.Unchanged
return eligible_symbols
def OnData(self, data):
target_date = self.Time.date()
if target_date < self.initial_date:
return
self.strategy_control.attempt_to_liquidate()
if not self.past_ear_values:
return
ear_list = self.past_ear_values
high_ear_threshold = np.percentile(ear_list, 90)
low_ear_threshold = np.percentile(ear_list, 10)
if target_date in self.earnings_announcement_data:
for symbol in self.earnings_announcement_data[target_date]:
ear_value = self.earnings_announcement_data[symbol][1]
if ear_value >= high_ear_threshold:
self.strategy_control.open_trade(symbol, True)
elif ear_value <= low_ear_threshold:
self.strategy_control.open_trade(symbol, False)
if self.Time.month % 3 == 0:
self.past_ear_values = list(self.recent_ear_values)
self.recent_ear_values.clear()