“该策略投资于免税分拆,通过ETF做空行业基准,排除应税分配,并维持等权重投资组合,以在对冲行业特定风险的同时捕捉分拆价值。”

I. 策略概要

该策略的目标是纽约证券交易所、纳斯达克和美国证券交易所的股票,重点关注免税分拆。投资者做多免税分拆公司,并通过ETF等方式做空匹配的行业基准。应税、混合税、资本回报分配和非自愿分配均被排除在外。投资组合采用等权重,利用免税分拆中的潜在价值创造,同时通过空头基准头寸对冲行业特定风险。

II. 策略合理性

该学术论文缺乏对分拆公司表现优异的理论解释,但其他研究将其归因于规模不经济。随着公司变得越来越复杂,决策管理和控制成本的增加可能会超过规模较大的好处。分拆简化了组织结构,降低了复杂性并提高了运营效率。此外,与部门合并为一个实体时相比,分拆公司的股权价值更清晰地表明了管理效率,从而更好地了解分离业务的真实业绩,并有助于其卓越的市场表现。

III. 来源论文

长期分拆回报的可预测性 [点击查看论文]

<摘要>

在最近一段时间里,购买并持有新分拆公司及其母公司的投资策略受到了投资界的广泛关注。尽管它们很受欢迎,但关于分拆吸引力的现有证据似乎是零散的。在本文中,我们详细研究了涵盖过去36年的综合样本中分拆公司及其母公司的股价表现。我们表明,在几乎所有考虑的持有期内,子公司和母公司的超额回报确实为正。

对于子公司而言,在对各种风险进行调整后,结果在经济上和统计上都显得显著。这一证据与投资者通过投资新分拆的子公司获得高于正常水平的回报率是一致的。然而,对于母公司而言,在纠正一个非常大的正异常值后,回报在统计上或经济上与零没有区别。

IV. 回测表现

年化回报19.4%
波动率N/A
β值0
夏普比率N/A
索提诺比率-0.532
最大回撤N/A
胜率45%

V. 完整的 Python 代码

from AlgorithmImports import *
import numpy as np
from typing import List, Dict
#endregion
class SpinOffAnomaly(QCAlgorithm):
    def Initialize(self) -> None:
        self.SetStartDate(2009, 1, 1) # spin off dates start at 2009
        self.SetCash(100_000)
        
        self.data: Dict[Symbol, SymbolData] = {}    # storing stocks sector
        self.tickers: List[str] = []                # storing tickers, which have spin offs
        self.selected_symbols: List[Symbol] = []    # storing stocks for trading
        self.managed_symbols: List[ManagedSymbol] = []
        
        self.holding_period: int = 6 * 21   # holding stocks and shorting ETF for n days
        self.max_traded_stocks: int = 25    # maximum number of trading max_traded_stocks
        self.leverage: int = 5
        
        self.etfs: Dict[int, str] = {
            104: 'VNQ',  # Vanguard Real Estate Index Fund
            311: 'XLK',  # Technology Select Sector SPDR Fund
            309: 'XLE',  # Energy Select Sector SPDR Fund
            206: 'XLV',  # Health Care Select Sector SPDR Fund
            103: 'XLF',  # Financial Select Sector SPDR Fund
            310: 'XLI',  # Industrials Select Sector SPDR Fund
            101: 'XLB',  # Materials Select Sector SPDR Fund
            205: 'XLY',  # Consumer Discretionary Select Sector SPDR Fund
            102: 'XLP',  # Consumer Staples Select Sector SPDR Fund
            207: 'XLU',  # Utilities Select Sector SPDR Fund    
            308: 'XLC'   # Communications Services
        }
        
        self.symbol: Symbol = self.AddEquity("SPY", Resolution.Daily).Symbol
        
        self.spin_offs: Symbol = self.AddData(QuantpediaSpinOffs, 'SPIN_OFFS', Resolution.Daily).Symbol
        
        for _, ticker in self.etfs.items():
            security = self.AddEquity(ticker, Resolution.Daily)
            security.SetFeeModel(CustomFeeModel())
            security.SetLeverage(self.leverage)
            
            # create SymbolData object for etf
            self.data[ticker] = SymbolData()
            
        self.selection_flag: bool = False
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.settings.daily_precise_end_time = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        
    def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())
            security.SetLeverage(self.leverage)
            
    def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
        # rebalance, when spin off tickers came
        if not self.selection_flag:
            return Universe.Unchanged
        self.selection_flag = False
        
        # select stocks, which had spin off
        selected: List[Symbol] = [x.Symbol for x in fundamental if x.HasFundamentalData and x.Symbol.Value in self.tickers and x.AssetClassification.MorningstarSectorCode] 
        
        etfs_tickers: List[str] = [ticker for _, ticker in self.etfs.items()]
        
        self.selected_symbols = []
        for stock in fundamental:
            symbol: Symbol = stock.Symbol
            ticker: str = symbol.Value
            
            # get last stock price for quantity calculation
            if symbol in selected:
                if symbol not in self.data:
                    self.data[symbol] = SymbolData()
                
                self.data[symbol].update_price(stock.AdjustedPrice)
            
                # get stock's sector
                sector: str = stock.AssetClassification.MorningstarSectorCode
                # store stock's sector
                self.data[symbol].update_sector(sector)
                self.selected_symbols.append(symbol)
            # update etfs prices
            if ticker in etfs_tickers:
                self.data[ticker].update_price(stock.AdjustedPrice)
        return self.selected_symbols
    def OnData(self, data: Slice) -> None:
        spin_offs_last_update_date: Dict[Symbol, datetime.date] = QuantpediaSpinOffs.get_last_update_date()
        if self.Securities[self.spin_offs].GetLastData() and self.Time.date() > spin_offs_last_update_date[self.spin_offs]:
            self.Liquidate()
            return
        if self.spin_offs in data and data[self.spin_offs]:
            self.tickers = [x for x in data[self.spin_offs].Tickers]
            self.selection_flag = True
         
        # storing managed symbols, which need to be removed from self.managed_symbols
        # and it's stock and ETF needs to be liquidated
        remove_managed_symbols: List[ManagedSymbol] = [] 
            
        for managed_symbol in self.managed_symbols:
            managed_symbol.holding_period += 1
            
            # stock has to be liquidate with it's ETF
            if managed_symbol.holding_period == self.holding_period:
                remove_managed_symbols.append(managed_symbol)
                
                # liquidate stock by selling it's quantity
                self.MarketOrder(managed_symbol.symbol, -managed_symbol.stock_quantity)
                # liquidate etf by buying it's quantity
                self.MarketOrder(managed_symbol.etf, managed_symbol.etf_quantity)
        
        # remove managed symbols from self.managed_symbols        
        for managed_symbol in remove_managed_symbols:
            self.managed_symbols.remove(managed_symbol)
        
        # trade only if there are some stocks selected from FineSelectionFunction
        if len(self.selected_symbols) == 0:
            return
        
        for symbol in self.selected_symbols:
            # check if there is a place for trading current stock
            if len(self.managed_symbols) < self.max_traded_stocks:
                # get stock's etf accoring to sector
                if symbol not in self.data:
                    continue
                etf: str = self.etfs[self.data[symbol].sector]
                
                # XLC doesn't have price
                if self.data[etf].last_price == 0:
                    continue
                
                # this weight corresponds to stock and stock's etf according to sector
                weight: float = self.Portfolio.TotalPortfolioValue / self.max_traded_stocks / 2
                # calculate stock quantity
                stock_quantity: int = np.floor(weight / self.data[symbol].last_price)
                # calculate etf quantity
                etf_quantity: int = np.floor(weight / self.data[etf].last_price)
                
                # create object of ManagedSymbol class, with stock's symbol,  stock's etf according to sector and their quantities
                managed_symbol: ManagedSymbol = ManagedSymbol(symbol, etf, stock_quantity, etf_quantity)
                
                # long stock
                self.MarketOrder(symbol, stock_quantity)
                # short etf
                self.MarketOrder(etf, -etf_quantity)
                
                # store created object of stock's ManagedSymbol
                self.managed_symbols.append(managed_symbol)
                
        # clear stocks, which had spin off
        self.selected_symbols.clear()
                
class ManagedSymbol():
    def __init__(self, symbol: Symbol, etf: str, stock_quantity: int, etf_quantity: int) -> None:
        self.holding_period: int = 0
        self.symbol: Symbol = symbol
        self.etf: str = etf
        self.stock_quantity: int = stock_quantity
        self.etf_quantity: int = etf_quantity
        
class SymbolData():
    def __init__(self) -> None:
        self.last_price: float = .0
        self.sector: int = 0
        
    def update_price(self, price: float) -> None:
        self.last_price = price
    def update_sector(self, sector: int) -> None:
        self.sector = sector
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaSpinOffs(PythonData):
    _last_update_date:Dict[Symbol, datetime.date] = {}
    @staticmethod
    def get_last_update_date() -> Dict[Symbol, datetime.date]:
       return QuantpediaSpinOffs._last_update_date
       
    def GetSource(self, config: SubscriptionDataConfig, date: datetime, isLiveMode: bool) -> SubscriptionDataSource:
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/equity/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
    def Reader(self, config: SubscriptionDataConfig, line: str, date: datetime, isLiveMode: bool) -> BaseData:
        data = QuantpediaSpinOffs()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit(): return None
        split: str = line.split(';')
        
        data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1)
        data['tickers'] = split[1:]
        if config.Symbol not in QuantpediaSpinOffs._last_update_date:
            QuantpediaSpinOffs._last_update_date[config.Symbol] = datetime(1,1,1).date()
        if data.Time.date() > QuantpediaSpinOffs._last_update_date[config.Symbol]:
            QuantpediaSpinOffs._last_update_date[config.Symbol] = data.Time.date()
        return data
# custom fee model
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 的更多信息

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

继续阅读