Quant BuffetRelax, Not Over Thinking

Advertising Effect within Stocks

Log in to collect

Academic paper

Advertising, Attention, and Stock Returns

AuthorsThomas J. Chemmanur; An Yan

Institute
  • Boston College
  • ?Boston College - Carroll School of Management
  • Fordham University
  • ?Fordham University - Gabelli School of Business

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.

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.

Backtest performance

Annualised return9.4%
Volatility3.2%
Beta0.022
Sharpe ratio1.69
Sortino ratio-0.142
Win rate51%

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"))