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.

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 Return9.4%
Volatility3.2%
Beta0.022
Sharpe Ratio1.69
Sortino Ratio-0.142
Maximum DrawdownN/A
Win Rate51%

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

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading