“该策略根据广告变化对股票进行排名,做空广告变化幅度最高的十分位,买入变化幅度最低的十分位,从 t+1 月中开始持有头寸六个月,并采用等权重分配。”

I. 策略概要

该策略针对市值超过2000万美元的NYSE、AMEX和NASDAQ股票。广告变化(ΔAdvt)通过计算从前一年到当年(广告年度)广告支出的对数差异来得出。每年,股票根据ΔAdvt的大小被分为十个分位数。一个零投资组合通过做空第10分位数(广告变化大的股票)并做多第1分位数(广告变化小的股票)来构建。股票在t+1年7月购买,持有六个月,按等权重分配,并基于完整的财务数据进行选择。

II. 策略合理性

学术研究将这一异常现象归因于投资者的有限注意力,这限制了他们评估所有投资和处理大量信息的能力。广告能够吸引注意力,增加买单或投资者信念的异质性,从而在广告年度推动股票价格上涨。然而,随着广告的注意力效应随时间减弱,股价往往会下降,导致未来的负回报。这表明,吸引注意力的广告可以暂时抬高股票价格,但随着广告影响力的减弱,这一效应会反转,从而在股价波动中形成一种可利用的模式,这与投资者行为和注意力动态有关。

III. 来源论文

Advertising, Attention, and Stock Returns [点击查看论文]

<摘要>

本文研究了广告对股票回报的影响,既包括短期影响,也包括长期影响。我们发现,广告投入的增加与广告年度的股票回报增大相关,但与广告年度后一年股票回报下降相关,即使在控制了其他股票价格预测因子(如公司规模、账面市值比和动量)后依然成立。我们推测,这种广告效应对股票回报的影响是由于广告对投资者注意力的作用。广告有助于公司吸引投资者的注意力。在广告年度,由于吸引的注意力,股价上涨,但在随后的年份,由于注意力随时间逐渐消退,股价会下跌。我们使用交易量和覆盖该公司股票的金融分析师数量来作为代理,测试“投资者注意力假设”。我们得出了五个一致的发现。首先,广告在广告年度内增加了公司在投资者中的可见度。其次,投资者注意力的增加与更大的现期股票回报和较小的未来股票回报相关。第三,广告对股票回报的影响在广告年度内可见度较高的公司中更为显著。特别是,当高广告投入的公司在股市中吸引更多投资者的注意力时,该公司股票回报在广告年度内增幅更大,而在随后的年份中下降幅度也更大。然而,如果广告年度吸引的注意力在广告年度后持续存在,那么这类高广告公司的股票回报下降的幅度较小。第四,如果投资者面临较大的套利成本,广告对未来股票回报的影响更强。最后,我们还发现,广告效应对于小型公司、价值公司以及前期股票表现或运营表现较差的公司来说更为显著。

IV. 回测表现

年化回报9.4%
波动率3.2%
β值0.022
夏普比率1.69
索提诺比率-0.142
最大回撤N/A
胜率51%

V. 完整的 Python 代码

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

发表评论

了解 Quant Buffet 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读