“该策略每年交易纽约证券交易所(NYSE)、美国证券交易所(AMEX)和纳斯达克(NASDAQ)股票,做多低“现金生产力”十分位的股票,做空高“现金生产力”十分位的股票,并在年度财务数据发布后重新平衡等权重仓位。”

I. 策略概要

该策略基于股票的“现金生产力”进行投资,现金生产力的计算公式为:(市场价值 − 总物理资产)/(现金 + 短期投资)。其中,市场价值包括股权市场价值和账面债务价值,而总物理资产则是资产的账面价值。股票每年根据现金生产力排序为十分位,最低生产力十分位的股票做多,最高生产力十分位的股票做空。仓位等权重,且每年重新平衡一次,通常在财务数据发布后的四月下旬进行。该策略利用了公司间现金生产力的低效性。

II. 策略合理性

学术研究表明,预期股票回报代表了公司的股本成本。生产力较低的公司面临较高的资本成本,从而导致较高的股本回报要求。随着现金生产力的增加,股票的风险性降低,导致预期回报下降。此外,投资者通过给予高生产力公司股价溢价来奖励它们,这进一步减少了它们的未来表现。因此,低现金生产力的公司提供更好的潜在回报,而高生产力的公司尽管风险较低,可能由于其溢价估值和风险回报权衡的减弱而表现不佳。这一动态构成了“现金生产力”投资策略的基础。

III. 来源论文

The Productivity of Cash and the Cross-Section of Expected Stock Returns [点击查看论文]

Satyajit Chandrashekar 和 Ramesh K. S. Rao。先进研究中心,State Street Global Advisors;麦克德莫特金融学教授,德克萨斯大学奥斯汀分校麦考姆商学院。

<摘要>

我们首先从分析角度表明,股票的预期回报、公司规模以及账面市值比(B/M)与“现金生产力”具有函数依赖关系,现金生产力定义为公司经济租金与其现金持有量的比例。然后,我们通过实证分析表明,现金生产力是一个高度显著且稳健的负向预测因素,能够预测股票回报。我们的研究表明,早期研究中公司规模和账面市值比的预测能力可能源自于它们作为现金生产力的代理变量——这一新的、具有经济理性解释的因素,能够解释股票回报的横截面表现。

IV. 回测表现

年化回报13%
波动率11%
β值-0.159
夏普比率1
索提诺比率0.106
最大回撤N/A
胜率58%

V. 完整的 Python 代码

from AlgorithmImports import *
from typing import List, Dict
from numpy import isnan
class ProductivityCashEffectStockMarket(QCAlgorithm):
    def Initialize(self)  -> None:
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        
        self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']
        self.fundamental_count:int = 3000
        self.fundamental_sorting_key = lambda x: x.MarketCap
        self.quantile:int = 10
        self.leverage:int = 10
        self.min_share_price:int = 5
        
        self.long:List[Symbol] = []
        self.short:List[Symbol] = []
        
        self.selection_month:int = 4
        self.selection_flag:bool = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.Schedule.On(self.DateRules.MonthEnd(market), self.TimeRules.AfterMarketOpen(market), self.Selection)
        self.settings.daily_precise_end_time = False
    def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
        for security in changes.AddedSecurities:
            security.SetLeverage(self.leverage)
            security.SetFeeModel(CustomFeeModel())
    def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
        if not self.selection_flag:
            return Universe.Unchanged
        
        selected:List[Fundamental] = [
            x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.Price > self.min_share_price and not \
            isnan(x.FinancialStatements.BalanceSheet.TotalDebt.TwelveMonths) and x.FinancialStatements.BalanceSheet.TotalDebt.TwelveMonths != 0 and not \
            isnan(x.FinancialStatements.BalanceSheet.TotalEquity.TwelveMonths) and x.FinancialStatements.BalanceSheet.TotalEquity.TwelveMonths != 0 and not \
            isnan(x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths) and x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths != 0 and not \
            isnan(x.FinancialStatements.BalanceSheet.CashAndCashEquivalents.TwelveMonths) and x.FinancialStatements.BalanceSheet.CashAndCashEquivalents.TwelveMonths != 0 and
            x.SecurityReference.ExchangeId in self.exchange_codes
        ]
        
        if len(selected) > self.fundamental_count:
            selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
        
        if len(selected) >= self.quantile:
            # Sorting by productivity of cash.    
            sorted_by_poc:List[Fundamental] = sorted(selected, key = lambda x:((x.FinancialStatements.BalanceSheet.TotalDebt.TwelveMonths + x.FinancialStatements.BalanceSheet.TotalEquity.TwelveMonths - \
                                                                                x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths) / \
                                                                                x.FinancialStatements.BalanceSheet.CashAndCashEquivalents.TwelveMonths), reverse=True)
            quantile = int(len(sorted_by_poc) / self.quantile)
            self.long = [x.Symbol for x in sorted_by_poc[-quantile:]]
            self.short = [x.Symbol for x in sorted_by_poc[:quantile]]
        return self.long + self.short
    
    def OnData(self, data: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False
        # Trade execution.
        targets:List[PortfolioTarget] = []
        for i, portfolio in enumerate([self.long, self.short]):
            for symbol in portfolio:
                if symbol in data and data[symbol]:
                    targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
        
        self.SetHoldings(targets, True)
        self.long.clear()
        self.short.clear()
    
    def Selection(self) -> None:
        if self.Time.month == self.selection_month:
            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"))

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读