该策略投资于纽交所、美国证券交易所和纳斯达克的股票,排除市值最小的25%的公司、美国存托凭证、封闭式基金和房地产投资信托基金。每季度,投资者寻找在盈利公告前30至15天内宣布股票回购计划的公司(回购至少涉及5%的已发行股票)。在盈利公告前10天至后15天之间,投资者做多这些回购股票,投资组合等权重分配并每日重新平衡。

策略概述

投资范围包括纽交所、美国证券交易所和纳斯达克的股票(不包括美国存托凭证、封闭式基金和房地产投资信托基金),市值最小的25%公司被排除。每季度,投资者寻找在盈利公告前30至15天内宣布股票回购计划的公司(回购计划至少涉及5%的已发行股票)。在盈利公告前10天至后15天之间,投资者做多这些宣布回购计划的股票。投资组合等权重分配,每日重新平衡。

策略合理性

学术研究指出,管理层通常比投资者掌握更多有关公司信息。鉴于这种信息不对称,管理层能够基于其信息做出明智的公司行动决策,如股票发行或回购。股票回购或二次股权发行的公告是自愿的,可以在几周或几个月内调整时间。因此,在盈利公告前宣布SEO(增发)或回购公告的时机可能会被视为有关公司未来盈利表现的重要信息。

论文来源

Are Earnings Predictable? [点击浏览原文]

<摘要>

我们发现,在股票发行和回购公告后发布的盈利公告会引发可预测的市场反应。在回购公告后发布的盈利公告的四因素异常回报率比在股票发行后发布的盈利公告高出5.1%,在(-1,+30)窗口内;当使用未调整的回报时,这一差异为2.2%。证据表明,市场并未完全反映自愿公司行为中所包含的信息。这些回报的漂移与盈利公告后漂移无关且截然不同。例如,即使公司回购公告伴随着负的盈利意外,其回报仍呈现正向漂移;而即使公司股票发行伴随正的盈利意外,其回报也出现负向漂移。由于该研究仅考察了盈利公告前后的短期窗口,避免了可能影响长期回报的基准误差。

回测表现

年化收益率25.2%
波动率11.11%
Beta1.632
夏普比率0.515
索提诺比率N/A
最大回撤68.8%
胜率26%

完整python代码

from AlgoLib import *
import numpy as np
#endregion

class EarningsAnnouncementsCombinedWithStockRepurchases(XXX):

    def Initialize(self):
        self.SetStartDate(2011, 1, 1) # Buyback data strats at 2011
        self.SetCash(100000) 
        
        self.fine = {}
        self.price = {}
        self.managed_symbols = []
        self.earnings_universe = []
        
        self.earnings = {}
        self.buybacks = {}
        
        self.max_traded_stocks = 40 # maximum number of trading stocks
        self.quantile = 4
        
        self.symbol = self.AddEquity("SPY", Resolution.Daily).Symbol
        
        # load earnings dates
        csv_data = self.Download('data.quantpedia.com/backtesting_data/economic/earning_dates.csv')
        lines = csv_data.split('\r\n')
        
        for line in lines:
            line_split = line.split(';')
            date = line_split[0]
            
            if date == '' :
                continue
            
            date = datetime.strptime(date, "%Y-%m-%d").date()
            self.earnings[date] = []
            
            for ticker in line_split[1:]: # skip date in current line
                self.earnings[date].append(ticker)
                
                if ticker not in self.earnings_universe:
                    self.earnings_universe.append(ticker)
        
        # load buyback dates
        csv_data = self.Download('data.quantpedia.com/backtesting_data/equity/BUY_BACKS.csv')
        lines = csv_data.split('\r\n')
        
        for line in lines[1:]: # skip header
            line_split = line.split(';')
            date = line_split[0]
            
            if date == '' :
                continue
            
            date = datetime.strptime(date, "%d.%m.%Y").date()
            self.buybacks[date] = []
            
            for ticker in line_split[1:]: # skip date in current line
                self.buybacks[date].append(ticker)
        
        self.months_counter = 0
        self.selection_flag = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        self.Schedule.On(self.DateRules.MonthStart(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)
        
    def OnSecuritiesChanged(self, changes):
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())
            security.SetLeverage(5)
            
    def CoarseSelectionFunction(self, coarse):
        # update stocks last prices
        for stock in coarse:
            ticker = stock.Symbol.Value
            
            if ticker in self.earnings_universe:
                # store stock's last price
                self.price[ticker] = stock.AdjustedPrice
        
        # rebalance quarterly
        if not self.selection_flag:
            return Universe.Unchanged
        self.selection_flag = False
        
        # select stocks, which had spin off
        selected = [x.Symbol for x in coarse if x.Symbol.Value in self.earnings_universe]

        return selected
        
    def FineSelectionFunction(self, fine):
        fine = [x for x in fine if x.MarketCap != 0 and 
                                    ((x.SecurityReference.ExchangeId == "NYS") or
                                    (x.SecurityReference.ExchangeId == "NAS") or 
                                    (x.SecurityReference.ExchangeId == "ASE"))]
        
        if len(fine) < self.quantile:
            return Universe.Unchanged
        
        # exclude 25% stocks with lowest market capitalization
        quantile = int(len(fine) / self.quantile)
        sorted_by_market_cap = sorted(fine, key = lambda x: x.MarketCap)
        selected = sorted_by_market_cap[quantile:]
        self.fine = {x.Symbol.Value : x.Symbol for x in selected}
        
        return list(self.fine.values())

    def OnData(self, data:Slice) -> None:
        remove_managed_symbols = []
        # maybe there should be BDay(15)
        liquidate_date = self.Time.date() - timedelta(15)
        
        # check if bought stocks have 15 days after earnings annoucemnet
        for managed_symbol in self.managed_symbols:
            if managed_symbol.earnings_date >= liquidate_date:
                remove_managed_symbols.append(managed_symbol)
                
                # liquidate stock by selling it's quantity
                self.MarketOrder(managed_symbol.symbol, -managed_symbol.quantity)
                
        # remove liquidated stocks from self.managed_symbols
        for managed_symbol in remove_managed_symbols:
            self.managed_symbols.remove(managed_symbol)
        
        # maybe there should be BDay(10)
        after_current = self.Time.date() + timedelta(10)
        
        if after_current in self.earnings:
            # this stocks has earnings annoucement after 10 days
            stocks_with_earnings = self.earnings[after_current]
            
            # 30 days before earnings annoucement
            buyback_start = self.Time.date() - timedelta(20)
            # 15 days before earnings annoucement
            buyback_end = self.Time.date() - timedelta(5)
            
            stocks_with_buyback = [] # storing stocks with buyback in period -30 to -15 days before earnings annoucement
            
            for buyback_date, tickers in self.buybacks.items():
                # check if buyback date is in period before earnings annoucement
                if buyback_date >= buyback_start and buyback_date <= buyback_end:
                    # iterate through each stock ticker for buyback date
                    for ticker in tickers:
                        # add stock ticker if it isn't already added, it has earnings annoucement after 10 days and was selected in fine
                        if (ticker not in stocks_with_buyback) and (ticker in stocks_with_earnings) and (ticker in self.fine):
                            stocks_with_buyback.append(self.fine[ticker])
                            
            # buying stocks buyback in period -30 to -15 days before earnings annoucement
            # and stocks, which have earnings date -10 days before current date
            for symbol in stocks_with_buyback:
                # check if there is a place in Portfolio for trading current stock
                if not len(self.managed_symbols) < self.max_traded_stocks:
                    continue
                
                # calculate stock quantity
                weight = self.Portfolio.TotalPortfolioValue / self.max_traded_stocks
                quantity = np.floor(weight / self.price[symbol.Value])
                
                # go long stock
                self.MarketOrder(symbol, quantity)
                
                # store stock's ticker, earnings date and traded quantity
                if symbol in data and data[symbol]:
                    self.managed_symbols.append(ManagedSymbol(symbol, after_current, quantity))
        
    def Selection(self):
        # quarterly selection
        if self.months_counter % 3 == 0:
            self.selection_flag = True
        self.months_counter += 1
        
class ManagedSymbol():
    def __init__(self, symbol, earnings_date, quantity):
        self.symbol = symbol
        self.earnings_date = earnings_date
        self.quantity = quantity
        
# custom fee model
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

Leave a Reply

Discover more from Quant Buffet

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

Continue reading