“该策略涉及根据账面市值比的不确定性对纽约证券交易所最大的500只股票进行排序,做多排名前十的股票,做空排名后十的股票,并每月重新平衡。”

I. 策略概要

投资范围包括纽约证券交易所最大的500只股票。为了计算账面市值比的不确定性(UNC),计算过去12个月每日预期账面市值比的标准差,并按其均值进行缩放。预期账面市值比基于估计的股权账面价值和净收入,并根据股息和市值进行调整。股票根据UNC分为十个等份,做多排名前十的股票,做空排名后十的股票。该策略采用价值加权,每月重新平衡,旨在从账面市值比的波动中获利。

II. 策略合理性

该论文发现高UNC股票与生产力和消费风险相关,导致由于风险补偿而产生更高的预期回报。高UNC股票也可能对系统性风险因素有更大的敞口,为其溢价提供了基于风险的解释。此外,高UNC股票可能反映了较低的信息质量和对未来盈利能力更大的不确定性,从而影响其账面市值比。该策略表明,最高UNC十分位的股票产生显著的正阿尔法,而最低十分位的股票显示出不显著的阿尔法,这表明高UNC股票表现优异。这种溢价无法用现有风险因素解释,并且该策略稳健,不受小盘股或非流动性股票的影响。

III. 来源论文

The Value Uncertainty Premium [点击查看论文]

<摘要>

我们调查了账面市值比(UNC)的时间序列波动性是否在股票回报中定价。UNC捕捉了公司现有资产和实物期权组合当前价值的不确定性,并反映了这些期权行使时的价内程度和不确定性变化。UNC还与信息风险、公司不灵活性和投资波动性相关,并获得正溢价。对高UNC公司做多、对低UNC公司做空的投资策略每年产生13%的风险调整回报。UNC溢价由面临更高信息风险的高UNC(不灵活)公司的出色表现驱动,并且无法用已建立的风险因素和公司特征来解释。

IV. 回测表现

年化回报12.01%
波动率14.62%
β值0.019
夏普比率0.55
索提诺比率0.199
最大回撤N/A
胜率50%

V. 完整的 Python 代码

from AlgorithmImports import *
from numpy import isnan
from typing import List, Dict
class TheValueUncertaintyPremium(QCAlgorithm):
    def Initialize(self) -> None:
        self.SetStartDate(2010, 1, 1)
        self.SetCash(100_000)
        self.exchange_codes: List[str] = ['NYS']
        self.weight: Dict[Symbol, float] = {}
        self.quantile: int = 10
        self.leverage: int = 10
        self.bm_by_symbol: Dict[Symbol, RollingWindow] = {}
        
        self.m_period: int = 12
        self.required_estimate_quarter: int = 4
        self.universe_selection_period: int = 12
        self.recent_EPS_estimate: Dict[Symbol, Dict[int, List[float]]] = {}
        
        market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        self.already_subscribed:list[Symbol] = []
        self.last_fundamental: List[Symbol] = []
        self.fundamental_count: int = 500
        self.fundamental_sorting_key = lambda x: x.DollarVolume
        self.selection_flag: bool = False
        self.rebalance_flag: bool = False
        self.UniverseSettings.Leverage = self.leverage
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.Schedule.On(self.DateRules.MonthStart(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
        self.selection_flag = False
        
        # select new universe once a period
        if self.Time.month % self.universe_selection_period == 0:
            selected: List[Fundamental] = [
                x for x in fundamental 
                if x.HasFundamentalData 
                and x.Market == 'usa' 
                and x.MarketCap != 0 
                and x.SecurityReference.ExchangeId in self.exchange_codes 
                and not isnan(x.ValuationRatios.ForwardDividend) and x.ValuationRatios.ForwardDividend
                and not isnan(x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths) and x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths != 0 
                and not isnan(x.EarningReports.BasicAverageShares.ThreeMonths) and x.EarningReports.BasicAverageShares.ThreeMonths != 0 
                and not isnan(x.FinancialStatements.BalanceSheet.CurrentLiabilities.ThreeMonths) and x.FinancialStatements.BalanceSheet.CurrentLiabilities.ThreeMonths
            ]
            if len(selected) > self.fundamental_count:
                selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
            for stock in selected:
                symbol: Symbol = stock.Symbol
                if symbol not in self.already_subscribed:
                    self.AddData(EstimizeEstimate, symbol)
                    self.already_subscribed.append(symbol)
            
            self.last_fundamental = selected
        self.rebalance_flag = True
        UNC: Dict[Symbol, float] = {}
        for stock in self.last_fundamental:
            symbol: Symbol = stock.Symbol
            
            # store pb value
            if symbol not in self.bm_by_symbol:
                self.bm_by_symbol[symbol] = RollingWindow[float](self.m_period)
            # calculate UNC
            if self.bm_by_symbol[symbol].IsReady:
                bm_values: List[float] = [x for x in self.bm_by_symbol[symbol]]
                bm_std: float = np.std(bm_values)
                pb_mean: float = np.mean(bm_values)
                UNC[stock] = bm_std / pb_mean
            # get net income estimate
            if symbol.Value in self.recent_EPS_estimate and self.Time.year in self.recent_EPS_estimate[symbol.Value]:
                current_be: float = stock.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths - stock.FinancialStatements.BalanceSheet.CurrentLiabilities.ThreeMonths
                
                # NOTE Expected net income for the end of fiscal year y, given the information available up to day d, is estimated as the product of expected earnings per share given by the mean of analysts’ forecasts up to day d and the total number of shares outstanding.
                estimated_net_income: float = np.mean(self.recent_EPS_estimate[symbol.Value][self.Time.year]) * stock.EarningReports.BasicAverageShares.ThreeMonths
                
                expected_dividend: float = stock.ValuationRatios.ForwardDividend
                expected_be: float = current_be + estimated_net_income - expected_dividend
                bm: float = expected_be / stock.MarketCap
                self.bm_by_symbol[symbol].Add(bm)
            else:
                if self.bm_by_symbol[symbol].Count != 0:
                    self.bm_by_symbol[symbol].Reset()
        if len(UNC) >= self.quantile:
            sorted_by_perf: List[Tuple[Symbol, float]] = sorted(UNC.items(), key = lambda x:x[1], reverse=True)
            quantile: int = int(len(sorted_by_perf) / self.quantile)
            long: List[Fundamental] = [x[0] for x in sorted_by_perf[:quantile]]
            short: List[Fundamental] = [x[0] for x in sorted_by_perf[-quantile:]]
            # market cap weighting
            for i, portfolio in enumerate([long, short]):
                mc_sum: float = sum(list(map(lambda stock: stock.MarketCap, portfolio)))
                for stock in portfolio:
                    self.weight[stock.Symbol] = ((-1)**i) * stock.MarketCap / mc_sum
        return list(self.weight.keys())
        
    def OnData(self, slice: Slice) -> None:
        # store latest EPS Estimize estimate
        estimize: Dict[Symbol, float] = slice.Get(EstimizeEstimate)
        for symbol, value in estimize.items():
            ticker: str = symbol.Value
            # store eond of the year estimates indexed by fiscal year
            if value.FiscalQuarter == self.required_estimate_quarter:
                if ticker not in self.recent_EPS_estimate:
                    self.recent_EPS_estimate[ticker] = {}
                if value.FiscalYear not in self.recent_EPS_estimate[ticker]:
                    self.recent_EPS_estimate[ticker][value.FiscalYear] = []
                self.recent_EPS_estimate[ticker][value.FiscalYear].append(value.Eps)
        if not self.rebalance_flag:
            return
        self.rebalance_flag = False
        # trade execution
        portfolio: List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weight.items() if slice.contains_key(symbol) and slice[symbol]]
        self.SetHoldings(portfolio, True)
        self.weight.clear()
    
    def Selection(self) -> None:
        self.selection_flag = True
# 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 的更多信息

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

继续阅读