Analyst Days
Log in to collectAcademic paper
Analyst Days, Stock Prices, and Firm Performance
Analyst Days, Stock Prices, and Firm Performance [Click to Open PDF]
- Ross School
- University of Michigan–Ann Arbor
- ?University of Michigan, Stephen M. Ross School of Business
- University of Pennsylvania
- National Bureau of Economic Research
- ?Bank of Israel
- ?National Bureau of Economic Research (NBER)
- ?University of Pennsylvania -- Wharton School of Business
Strategy in a nutshell
Trades NYSE, AMEX, and NASDAQ stocks around analyst days, going long on the stock on the event day and holding for 20 days. Portfolios are value-weighted to capture potential abnormal returns from market attention.
Economic rationale
Analyst days convey predominantly positive firm information, leading to market underreaction. Persistent abnormal returns and improved firm metrics post-event create exploitable opportunities, as investors gradually adjust to credible disclosures.
Backtest performance
Annualised return18.3%
Volatility24.82%
Beta0.355
Sharpe ratio0.59
Sortino ratio-0.005
Win rate59%
Full Python code
from AlgorithmImports import *
#endregion
class AnalystDays(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100_000)
self.equally_weighted_flag: bool = False # False - VW; True - EW
self.analyst_days: Dict[str, List[datetime.date]] = {}
self.selected: List[Fundamental] = []
self.tickers: List[str] = []
self.market_cap:dict[Symbol, float] = {}
market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.spy_future_symbol: Symbol = market
self.opened_long_positions_period: Dict[Symbol, int] = {} # opened long stock postions with holding period
self.holding_period: int = 20 # n days of holding period
self.leverage: int = 10
csv_string_file: str = self.Download('data.quantpedia.com/backtesting_data/economic/analyst_days.csv')
lines: List[str] = csv_string_file.split('\r\n')
header: List[str] = lines[0].split(';')
for ticker in header[1:]: # skip first column == date
self.tickers.append(ticker)
for line in lines[1:]: # skip header line
if line == '':
continue
line: List[str] = line.split(';')
# convert string to date
str_date: str = line[0]
date: datetime.date = datetime.strptime(str_date, '%d.%m.%Y').date()
for index in range(1, len(line)): # skip date as a first value of the row
# retrive ticker from list created based on csv header
ticker: str = self.tickers[index - 1]
if ticker not in self.analyst_days:
self.analyst_days[ticker] = []
# if analyst_day_flag == 1, then this company had analyst days
analyst_day_flag: str = line[index]
if analyst_day_flag == '1':
# company had analyst days
self.analyst_days[ticker].append(date)
self.selection_flag: bool = False
self.UniverseSettings.Leverage = self.leverage
self.UniverseSettings.Resolution = Resolution.Minute
self.AddUniverse(self.FundamentalSelectionFunction)
self.settings.daily_precise_end_time = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.Schedule.On(self.DateRules.MonthStart(market), self.TimeRules.BeforeMarketClose(market, 0), self.Selection)
self.Schedule.On(self.DateRules.EveryDay(market), self.TimeRules.BeforeMarketClose(market, 1), self.ManageTrade)
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
# rebalance monthly
if not self.selection_flag:
return Universe.Unchanged
self.selection_flag = False
# select S&P500 stocks from fundamental based on ticker
self.selected = [
x for x in fundamental
if x.MarketCap != 0
and x.Symbol.Value in self.analyst_days
]
if self.equally_weighted_flag:
self.selected_values = list(map(lambda stock: stock.Symbol, self.selected))
return self.selected_values
else:
self.market_cap = { stock.Symbol : stock.MarketCap for stock in self.selected }
return list(self.market_cap.keys())
def ManageTrade(self):
# liquidate opened symbols
symbols_to_remove: List[Symbol] = []
rebalance_flag: bool = False
for symbol in self.opened_long_positions_period:
holding_period_remaining: int = self.opened_long_positions_period[symbol]
if holding_period_remaining == 0:
# remove stock from holdings
symbols_to_remove.append(symbol)
else:
# decrement remaining holding period
self.opened_long_positions_period[symbol] -= 1
for symbol in symbols_to_remove:
self.Liquidate(symbol)
del self.opened_long_positions_period[symbol]
rebalance_flag = True # rebalance is required
today: datetime.date = self.Time.date()
long: List[Symbol] = [] # storing symbols of stocks of those companies that had analyst day
# check if selected companies had analyst day
for stock in self.selected:
symbol: Symbol = stock.Symbol
ticker: str = symbol.Value
# make sure selected stock has analyst days data
if ticker not in self.analyst_days:
continue
if today in self.analyst_days[ticker]:
# store stock in opened postions selection
self.opened_long_positions_period[symbol] = self.holding_period
rebalance_flag = True # rebalance is required
# rebalance is required
if rebalance_flag:
# rebalance whole active selection
if self.equally_weighted_flag:
n: int = len(self.opened_long_positions_period)
if n != 0:
# rebalance whole trade selection
for symbol in self.opened_long_positions_period:
price = self.Securities[symbol].Price
if price != 0:
self.SetHoldings(symbol, 1/n)
else:
total_cap: float = sum([self.market_cap[x] for x in self.opened_long_positions_period])
for symbol in self.opened_long_positions_period:
if symbol in self.market_cap:
self.SetHoldings(symbol, self.market_cap[symbol] / total_cap)
def Selection(self) -> None:
self.selection_flag = True
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
fee: float = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))