
The strategy ranks stocks by advertising changes, shorting high-change decile and buying low-change decile, holding positions for six months, starting mid-t+1, with equal weighting.
ASSET CLASS: stocks | REGION: United States | FREQUENCY:
6 Months | MARKET: equities | KEYWORD: Advertising Effect, Stocks
I. STRATEGY IN A NUTSHELL: Semiannual U.S. Equity Advertising Change Decile Strategy
This semiannual strategy targets U.S. stocks (NYSE, AMEX, NASDAQ) with market caps above $20M. Stocks are ranked annually by advertising change (ΔAdvt). A zero-investment portfolio is formed by going long on the lowest ΔAdvt decile and shorting the highest. Positions are initiated in month 7 of year t+1 and held for six months, equally weighted.
II. ECONOMIC RATIONALE
The anomaly arises from limited investor attention. High advertising temporarily attracts investor focus, inflating stock prices. As the effect fades, prices decline, producing predictable negative returns. Exploiting this attention-driven pattern enables systematic strategies based on advertising-induced price reversals.
III. SOURCE PAPER
Advertising, Attention, and Stock Returns [Click to Open PDF]
Thomas Chemmanur, Boston College – Carroll School of Management; An Yan, Fordham University – Gabelli School of Business
<Abstract>
This paper studies the effect of advertising on stock returns both in the short run and in the long run. We find that a greater amount of advertising is associated with a larger stock return in the advertising year but a smaller stock return in the year subsequent to the advertising year, even after we control for other price predictors, such as size, book-to-market, and momentum. We conjecture that this advertising effect on stock returns is due to the effect of advertising on investor attention. Advertising could help a firm attract investors’ attention. Stock price increases in the adverting year due to the attracted attention, but decreases in the subsequent year as the attracted attention wears out over time in the long run. We test this “investor attention hypothesis” using trading volume and the number of financial analysts covering to proxy for investors’ attention on the firm’s stock. We document five consistent findings. First, advertising increases a firm’s visibility among investors in the advertising year. Second, an increased level of investor attention is associated with a larger contemporary stock return and a smaller future stock return. Third, the effect of advertising on stock returns is stronger in firms with more visibility in the advertising year. In particular, when a high advertising firm attracts more investor attention in the stock market, the stock return of the high advertising firm increases to a larger degree in the contemporary adverting year and decreases to a larger degree in the subsequent years. However, the stock return of such a high advertising firm decreases to a smaller degree if the attention attracted in the advertising year persists subsequent to the advertising year. Fourth, the effect of advertising on future stock returns is stronger if investors face a larger cost of arbitrage. Finally, we also find that the advertising effect is stronger for small firms, value firms, and firms with poor ex-ante stock performance or poor ex-ante operating performance.


IV. BACKTEST PERFORMANCE
| Annualised Return | 9.4% |
| Volatility | 3.2% |
| Beta | 0.022 |
| Sharpe Ratio | 1.69 |
| Sortino Ratio | -0.142 |
| Maximum Drawdown | N/A |
| Win Rate | 51% |
V. FULL PYTHON CODE
from AlgorithmImports import *
from typing import Dict, List
import numpy as np
class AdvertisingEffect(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2000, 1, 1)
self.SetCash(100_000)
self.UniverseSettings.Leverage = 10
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.exchange_codes: List[str] = ['NYS', 'NAS', 'ASE']
self.fundamental_count: int = 3_000
self.fundamental_sorting_key = lambda x: x.MarketCap
self.min_market_cap: int = 20_000_000
self.quantile: int = 10
self.buy_month: int = 6
self.sell_month: int = 12
self.selection_flag: bool = False
self.adv_expenses: Dict[Symbol, float] = {}
self.long_symbols: List[Symbol] = []
self.short_symbols: List[Symbol] = []
self.settings.daily_precise_end_time = False
self.settings.minimum_order_margin_portfolio_percentage = 0.
market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.Schedule.On(self.DateRules.MonthEnd(market), self.TimeRules.AfterMarketOpen(market), self.Selection)
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
if not self.selection_flag:
return Universe.Unchanged
filtered: List[Fundamental] = [
f for f in fundamental if f.HasFundamentalData
and f.SecurityReference.ExchangeId in self.exchange_codes
and f.MarketCap > self.min_market_cap
and not np.isnan(f.FinancialStatements.IncomeStatement.SellingAndMarketingExpense.ThreeMonths)
and f.FinancialStatements.IncomeStatement.SellingAndMarketingExpense.ThreeMonths > 0
]
sorted_filter: List[Fundamental] = sorted(filtered,
key=self.fundamental_sorting_key,
reverse=True)[:self.fundamental_count]
d_adv: Dict[Symbol, float] = {}
for f in sorted_filter:
if f.Symbol not in self.adv_expenses:
self.adv_expenses[f.Symbol] = -1
adv_expenses: float = f.FinancialStatements.IncomeStatement.SellingAndMarketingExpense.ThreeMonths
if f.Symbol in self.adv_expenses and self.adv_expenses[f.Symbol] != -1:
d_adv[f.Symbol] = adv_expenses / self.adv_expenses[f.Symbol] - 1
# Update adv expense value
self.adv_expenses[f.Symbol] = adv_expenses
# NOTE: Get rid of old advertisment records so we work with latest values
for symbol in self.adv_expenses:
if symbol not in [x.Symbol for x in sorted_filter]:
self.adv_expenses[symbol] = -1
if len(d_adv) >= self.quantile:
sorted_by_adv: list = sorted(d_adv.items(), key=lambda x: x[1], reverse=True)
decile: int = int(len(sorted_by_adv) / self.quantile)
self.long_symbols = [x[0] for x in sorted_by_adv[-decile:]]
self.short_symbols = [x[0] for x in sorted_by_adv[:decile]]
return self.long_symbols + self.short_symbols
def OnData(self, slice: Slice) -> None:
if not self.selection_flag:
return
self.selection_flag = False
# Trade execution
targets: List[PortfolioTarget] = []
for i, portfolio in enumerate([self.long_symbols, self.short_symbols]):
for symbol in portfolio:
if slice.ContainsKey(symbol) and slice[symbol] is not None:
targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
self.SetHoldings(targets, True)
self.long_symbols.clear()
self.short_symbols.clear()
def Selection(self) -> None:
if self.Time.month == self.buy_month:
self.selection_flag = True
elif self.Time.month == self.sell_month:
self.Liquidate()
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
fee: float = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))
VI. Backtest Performance