数据集涵盖NYSE、Amex和NASDAQ上市公司,财务报表来自Compustat,股票信息来自CRSP。现金持有是现金及现金等价物占总资产的比例,净经营资产(NOA)为运营资产与负债的差额。投资组合按现金持有分为十分位,NOA分为三分位,做多现金高、NOA低的公司,做空现金低、NOA高的公司,等权重配置并按月重新平衡。

策略概述

数据集包括所有在纽约证券交易所(NYSE)、美国证券交易所(Amex)和纳斯达克(NASDAQ)上市的公司。公司的年度财务报表来自Compustat。月度股票信息来自证券价格研究中心(CRSP)。就公司特征而言,现金持有是公司持有的现金和现金等价物占总资产的比例。净经营资产(NOA)是财政年度t末运营资产与负债的差额,按滞后的总资产进行调整。将t+1年7月至t+2年6月的月度股票回报与t年度的财务报表匹配。将投资组合按现金持有分为十分位,按净经营资产分为三分位。做多现金持有较高且NOA较低的公司,做空现金持有较低且NOA较高的公司。投资组合为等权重,并按月重新平衡。

策略合理性

结果表明,现金持有效应无法完全通过基于风险的解释来解释。更可能的是,它与应计项目相关的异常、错误定价和套利限制有关。现金持有效应似乎存在于现金持有较高且净经营资产(NOA)较低的公司中。根据有限注意力理论,投资者倾向于关注会计盈利能力,而忽视了与NOA负相关的现金盈利能力。那些过去报告会计业绩较差的低NOA公司会引发投资者的悲观情绪。这就是为什么它们往往被低估,并在低估得到纠正后获得较高的平均回报。因此,现金持有效应是行为故事的体现,特别是NOA效应,这是股票回报横截面中的一个众所周知的模式。

论文来源

What is the Real Relationship between Cash Holdings and Stock Returns? [点击浏览原文]

<摘要>

关于现金持有与平均股票回报之间关系的文献提供了混合证据。我们通过实证验证,发现这种关系是正向的,并且在风险调整、现金持有投资组合的构建以及投资组合回报的加权方案下具有稳健性。我们进一步研究了一系列可能解释这一正向关系的渠道。我们发现,现金持有效应可以被应计项目相关的异常所取代,主要来自于低净经营资产(NOA)的股票。它在那些套利限制较大的股票中更为强烈。总体而言,我们的结果表明,现金持有效应并不代表一种新的资产定价规律,而是与现有的异常现象密切相关,并且与错误定价相关。

回测表现

年化收益率9.25%
波动率17.26%
Beta-0.022
夏普比率0.54
索提诺比率N/A
最大回撤N/A
胜率47%

完整python代码

from AlgorithmImports import *
# endregion

class CashHoldingsEffectAndNetOperatingAssets(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        
        self.leverage:int = 5
        self.cash_holdings_quantile:int = 5
        self.net_operating_assets_quantile:int = 2
        self.three_months_flag:bool = True
        
        self.weights:Dict[Symbol, float] = {}

        self.market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol

        self.coarse_count:int = 3000
        self.selection_flag:bool = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)

        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.        
        self.Schedule.On(self.DateRules.MonthStart(self.market), self.TimeRules.BeforeMarketClose(self.market, 0), self.Selection)

    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]:
        if not self.selection_flag:
            return Universe.Unchanged
        
        fundamental = [x for x in fundamental if not np.isnan(x.FinancialStatements.BalanceSheet.Cash.ThreeMonths) and x.FinancialStatements.BalanceSheet.Cash.ThreeMonths != 0 and \
            not np.isnan(x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths) and x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths != 0 and \
            not np.isnan(x.FinancialStatements.BalanceSheet.FinancialAssets.ThreeMonths) and x.FinancialStatements.BalanceSheet.FinancialAssets.ThreeMonths != 0 and \
            not np.isnan(x.FinancialStatements.BalanceSheet.TotalLiabilitiesAsReported.ThreeMonths) and x.FinancialStatements.BalanceSheet.TotalLiabilitiesAsReported.ThreeMonths != 0
            ]

        if self.coarse_count <= 1000:
            selected:List = sorted([x for x in fundamental if x.HasFundamentalData and x.Market == 'usa'],
                    key=lambda x: x.DollarVolume, reverse=True)[:self.coarse_count]
        else:
            selected:List = list(filter(lambda stock: stock.MarketCap != 0, [x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.AdjustedPrice >= 5]))[-self.coarse_count:]

        cash_holdings:Dict[Symbol, float] = {}
        net_operating_assets:Dict[Symbol, float] = {}

        for stock in selected:
            symbol:Symbol = stock.Symbol

            cash:float = stock.FinancialStatements.BalanceSheet.Cash.ThreeMonths if self.three_months_flag \
                else stock.FinancialStatements.BalanceSheet.Cash.TwelveMonths
            total_assets:float = stock.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths if self.three_months_flag \
                else stock.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths
            financial_assets:float = stock.FinancialStatements.BalanceSheet.FinancialAssets.ThreeMonths if self.three_months_flag \
                else stock.FinancialStatements.BalanceSheet.FinancialAssets.TwelveMonths
            total_liabilities:float = stock.FinancialStatements.BalanceSheet.TotalLiabilitiesAsReported.ThreeMonths if self.three_months_flag \
                else stock.FinancialStatements.BalanceSheet.TotalLiabilitiesAsReported.TwelveMonths

            if all([cash, total_assets, financial_assets, total_liabilities]):
                cash_holdings_value:float = cash / total_assets
                cash_holdings[symbol] = cash_holdings_value

                operating_assets:float = total_assets - financial_assets
                net_operating_assets_value:float = (operating_assets - total_liabilities) / total_assets
                net_operating_assets[symbol] = net_operating_assets_value

        if len(cash_holdings) < self.cash_holdings_quantile or len(net_operating_assets) < self.net_operating_assets_quantile:
            return Universe.Unchanged

        cash_holdings_quantile:int = int(len(cash_holdings) / self.cash_holdings_quantile)
        sorted_by_cash_holdings:List[Symbol] = [x[0] for x in sorted(cash_holdings.items(), key=lambda item: item[1])]
        high_cash_holdings:List[Symbol] = sorted_by_cash_holdings[-cash_holdings_quantile:]
        low_cash_holdings:List[Symbol] = sorted_by_cash_holdings[:cash_holdings_quantile]
        
        net_operating_assets_quantile:int = int(len(net_operating_assets) / self.net_operating_assets_quantile)
        sorted_by_net_op_assets:List[Symbol] = [x[0] for x in sorted(net_operating_assets.items(), key=lambda item: item[1])]
        low_net_operating_assets:List[Symbol] = sorted_by_net_op_assets[:net_operating_assets_quantile]
        high_net_operating_assets:List[Symbol] = sorted_by_net_op_assets[-net_operating_assets_quantile:]

        long:List[Symbol] = [symbol for symbol in high_cash_holdings if symbol in low_net_operating_assets]
        short:List[Symbol] = [symbol for symbol in low_cash_holdings if symbol in high_net_operating_assets]

        for i, portfolio in enumerate([long, short]):
            for symbol in portfolio:
                self.weights[symbol] = ((-1) ** i) / len(portfolio)

        return list(self.weights.keys())
        
    def OnData(self, data: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        # trade execution
        invested:List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
        for symbol in invested:
            if symbol not in self.weights:
                self.Liquidate(symbol)
                
        for symbol, w in self.weights.items():
            if symbol in data and data[symbol]:
                self.SetHoldings(symbol, w)
                
        self.weights.clear()
        
    def Selection(self) -> None:
        self.selection_flag = True

# 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