财报公告期间机构持股效应
登录后收藏学术论文
Overpricing: Evidence from Earnings Announcements
伯克曼
- ?科赫,奥克兰大学商学院,爱荷华州立大学金融系
- ?爱荷华州立大学金融系
在财报发布前的几天,我们发现那些已经可能被高估的股票(即低机构持股、同时具备高市净率、周转率、波动率或分析师预测分歧的股票)平均价格上涨接近1%。然而,在财报发布后的几天,这些相同的股票会产生超过3%的负异常收益。综合来看,这些结果表明,对于容易被高估的股票,财报发布后会出现显著的净修正。这些结果与Miller(1977)提出的乐观偏误假设一致,也与近期的证据相符,即跨部门的收益预测可预测性主要集中在低机构持股的股票上。
策略概要
该策略以标准普尔3000指数的股票为目标,重点关注那些机构持股最低且交易量最高的股票。投资者选择临近财报发布日期的股票,在财报发布前两天建立多头头寸,并在财报发布后两天建立空头头寸。头寸按等权重分配,投资组合每日再平衡,以保持与策略的一致性。
策略合理性
学术研究表明,由于乐观投资者的过度定价通常会在财报发布时得到修正,因为相关信息的发布减少了意见分歧。在财报发布前,乐观的投资者可能会暂时增加持股,猜测财报结果。这种购买压力,再加上卖空限制和低机构持股,进一步加剧了过度定价。因此,容易被高估的股票(低机构持股和高预测值)在公告前会看到最强的价格上涨。在财报发布后,发生了两个效应:投机性头寸被平仓,导致价格逆转;而财报发布揭示了过度乐观,进一步导致价格下跌。这些可预测的模式突出了行为因素对公告前后股价的影响。
回测表现
索提诺比率-0.112
胜率50%
完整 Python 代码
import data_tools
from AlgorithmImports import *
import numpy as np
from pandas.tseries.offsets import BDay
class InstititutionalOwnershipEffectDuringEarningsAnnouncements(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2009, 1, 1) # earnings dates starts at 2010
self.SetCash(100_000)
self.period: int = 21
self.lookup_period: int = 2
self.holding_period: int = 2
self.min_share_price: int = 5
self.leverage: int = 5
self.quantile: int = 5
self.total_long_num: int = 15
self.total_short_num: int = 15
self.long: Set(Symbol) = set()
self.short: Set(Symbol) = set()
self.data: Dict[Symbol, data_tools.SymbolData] = {}
self.earnings_data: Dict[datetime.date, list[str]] = {}
self.first_date: Union[None, datetime.date] = None
earnings_set: Set(str) = set()
earnings_data: str = self.Download('data.quantpedia.com/backtesting_data/economic/earnings_dates_eps.json')
earnings_data_json: List[dict] = json.loads(earnings_data)
for obj in earnings_data_json:
date: datetime.date = datetime.strptime(obj['date'], "%Y-%m-%d").date()
if not self.first_date: self.first_date = date
self.earnings_data[date] = []
for stock_data in obj['stocks']:
ticker: str = stock_data['ticker']
self.earnings_data[date].append(ticker)
earnings_set.add(ticker)
self.tickers: List[str] = list(earnings_set)
market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
# equally weighted brackets for traded symbols. - n symbols long, m symbols short, 2 days of holding
self.trade_manager: data_tools.TradeManager = data_tools.TradeManager(
self, self.total_long_num, self.total_short_num, self.holding_period)
self.fundamental_count: int = 3000
self.fundamental_sorting_key = lambda x: x.MarketCap
self.selection_flag: bool = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.settings.daily_precise_end_time = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Schedule.On(self.DateRules.MonthStart(market), self.TimeRules.AfterMarketOpen(market), self.Selection)
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(data_tools.CustomFeeModel())
security.SetLeverage(self.leverage)
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
# update the rolling window every day
for stock in fundamental:
symbol: Symbol = stock.Symbol
if symbol in self.data:
self.data[symbol].update(stock.Volume)
if not self.selection_flag:
return Universe.Unchanged
self.selection_flag = False
selected: List[Fundamental] = [
x for x in fundamental if x.Symbol.Value in self.tickers \
and x.HasFundamentalData and x.MarketCap != 0 and x.Market == 'usa' and x.Price > self.min_share_price
]
if len(selected) > self.fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
warmed_up_symbols:List[Symbol] = []
for stock in selected:
symbol: Symbol = stock.Symbol
if symbol not in self.data:
self.data[symbol] = data_tools.SymbolData(symbol, self.period)
history: DataFrame = self.History(symbol, self.period, Resolution.Daily)
if history.empty:
continue
if not hasattr(history.loc[symbol], 'volume'):
continue
volumes: Series = history.loc[symbol].volume
for _, volume in volumes.items():
self.data[symbol].update(volume)
if self.data[symbol].is_ready():
warmed_up_symbols.append(symbol)
if len(warmed_up_symbols) < self.quantile:
return Universe.Unchanged
quantile: int = int(len(warmed_up_symbols) / self.quantile)
lowest_market_caps: List[Symbol] = [x for x in warmed_up_symbols[-quantile:]]
volumes: Dict[Symbol, float] = { x : self.data[x].sum_volumes() for x in warmed_up_symbols}
quantile: int = int(len(volumes) / self.quantile)
highest_volumes: List[Symbol] = [x[0] for x in sorted(volumes.items(), key=lambda item: item[1])][-quantile:]
self.long = set(symbol for symbol in lowest_market_caps if symbol in highest_volumes)
return list(self.long)
def OnData(self, data: Slice) -> None:
# liquidate opened symbols after self.holding_period days.
self.trade_manager.TryLiquidate()
# long two days before earnings annoucement
date_to_lookup_long: datetime.date = (self.Time + BDay(self.lookup_period)).date()
# short two days after earnings annoucement
date_to_lookup_short: datetime.date = (self.Time - BDay(self.lookup_period)).date()
if date_to_lookup_long < self.first_date:
self.long.clear()
# open new trades
symbols_to_delete: List[Symbol] = []
if date_to_lookup_long in self.earnings_data:
for symbol in self.long:
if symbol.Value in self.earnings_data[date_to_lookup_long] and symbol in data and data[symbol]:
self.trade_manager.Add(symbol, True)
symbols_to_delete.append(symbol)
# delete already traded symbols and add them to short portfolio
for symbol in symbols_to_delete:
self.long.remove(symbol)
self.short.add(symbol)
symbols_to_delete.clear()
if date_to_lookup_short in self.earnings_data:
for symbol in self.short:
if symbol.Value in self.earnings_data[date_to_lookup_short] and symbol in data and data[symbol]:
self.trade_manager.Add(symbol, False)
symbols_to_delete.append(symbol)
for symbol in symbols_to_delete:
self.short.remove(symbol)
def Selection(self) -> None:
self.selection_flag = True