“该策略投资于按账面市值比(B/M)股票比率排序的价值加权十分位投资组合。如果CRSP价值加权指数的24个月回报为负,则做多最高十分位,做空最低十分位。”

I. 策略概要

投资范围包括来自Kenneth French数据库的按账面市值比(B/M)股票比率排序的价值加权十分位投资组合。每个月,根据CRSP价值加权指数的24个月回报来确定市场状态。如果回报为负,则策略做多最高十分位投资组合,做空最低十分位投资组合。持仓六个月。该策略使用账面市值比(B/M)比率作为排序标准,并持仓半年。

II. 策略合理性

价值溢价通常用这样一种观点来解释,即价值投资者押注于容易对新闻反应过度、推断过去的增长并追随股价趋势的幼稚投资者。行为理论认为,当动量交易减少时,价值投资者会受益,因为这减少了对过去表现不佳的定价过低的股票的抛售压力。相反,动量交易的增加会推高定价过高的股票的价格,从而使避免这些股票的价值策略受益。此外,在市场下跌之后,定价过低的股票通常会获得更高的回报,因为过去的过度反应导致这些股票未来表现更加有利。

III. 来源论文

Value Bubbles [点击查看论文]

<摘要>

该研究揭示,价值策略的历史表现主要由偶尔出现的泡沫驱动。行为理论认为,价值溢价应随长期滞后的市场回报或其他投资者情绪的总体代理变量而变化——我们的横截面测试支持了这一假设。从1926年到2022年,我们发现,在市场回报连续两年为负之后,美国价值溢价约为其无条件对应物的3倍,而在市场回报连续两年为正之后,价值溢价似乎消失了。此外,动量策略出现重大亏损的时期(动量崩盘)往往与价值策略出现重大盈利的时期(价值泡沫)相吻合。利用这些见解,我们开发了一种正偏斜、可实施的动态投资策略,将标准价值和动量策略的夏普比率分别提高了三倍以上和60%。我们的研究结果具有国际稳健性。

IV. 回测表现

年化回报3.68%
波动率3.45%
β值0.104
夏普比率1.07
索提诺比率-0.113
最大回撤N/A
胜率52%

V. 完整的 Python 代码

from AlgorithmImports import *
from numpy import floor
#endregion
class ValueFactorAfterNegativeMarketReturn(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        self.coarse_count = 500
        
        self.period = 24 * 21
        self.SetWarmUp(self.period, Resolution.Daily)
        self.quantile = 10
        # Trenching
        self.holding_period = 6
        self.managed_queue = []
        
        self.symbol = self.AddEquity('VTI', Resolution.Daily).Symbol
        self.data = RollingWindow[float](self.period)
        self.selection_flag = False
        
        self.recent_price = {}  # recent stock prices
        
        self.UniverseSettings.Resolution = Resolution.Daily
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.settings.daily_precise_end_time = False
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)
        
    def OnSecuritiesChanged(self, changes):
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())
            security.SetLeverage(10)
    def CoarseSelectionFunction(self, coarse):
        for stock in coarse:
            symbol = stock.Symbol
            # append recent price to market symbol
            if symbol == self.symbol:
                self.data.Add(stock.AdjustedPrice)
            # store monthly stock prices
            if self.selection_flag:
                self.recent_price[symbol] = stock.AdjustedPrice
            
        if not self.selection_flag:
            return Universe.Unchanged
        selected = sorted([x for x in coarse if x.HasFundamentalData and x.Price > 5],
            key=lambda x: x.DollarVolume, reverse=True)[:self.coarse_count]
        return [x.Symbol for x in selected]
    def FineSelectionFunction(self, fine):
        fine = [x for x in fine if x.MarketCap != 0 and x.ValuationRatios.PBRatio != 0]
        
        # market data is ready
        if self.data.IsReady:
            market_ret:float = self.data[0] / self.data[self.period-1] - 1
            if market_ret >= 0:
                return Universe.Unchanged
        else:
            return Universe.Unchanged
        bm_ratio = {}
        market_cap = {}
        for stock in fine:
            symbol = stock.Symbol
            
            market_cap[symbol] = stock.MarketCap
            bm_ratio[symbol] = 1 / stock.ValuationRatios.PBRatio
        
        long:list = []
        short:list = []
        
        if len(bm_ratio) >= self.quantile:
            # BM ratio sorting 
            sorted_by_bm = sorted(bm_ratio.items(), key=lambda x: x[1], reverse=True)
            decile = int(len(sorted_by_bm) / self.quantile)
            
            # Long the highest decile portfolio 
            # Short the lowest decile portolio
            long = [x[0] for x in sorted_by_bm[:decile]]
            short = [x[0] for x in sorted_by_bm[-decile:]]
        
            # Market cap weighting
            equity = self.Portfolio.TotalPortfolioValue / self.holding_period
            
            weights = {}
            
            total_market_cap_long = sum([market_cap[sym] for sym in long if sym in market_cap])
            for symbol in long:
                if symbol in market_cap:
                    weights[symbol] = market_cap[symbol] / total_market_cap_long
            
            total_market_cap_short = sum([market_cap[sym] for sym in short if sym in market_cap])
            for symbol in short:
                if symbol in market_cap:
                    weights[symbol] = -market_cap[symbol] / total_market_cap_short
            
            symbol_q = [(symbol, floor((equity*symbol_w) / self.recent_price[symbol])) for symbol,symbol_w in weights.items()]
            self.managed_queue.append(RebalanceQueueItem(symbol_q))
                
        return long + short
    def OnData(self, data):
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        # Trade execution
        remove_item = None
        
        # Rebalance portfolio
        for item in self.managed_queue:
            if item.holding_period == self.holding_period:
                for symbol, quantity in item.symbol_q:
                    self.MarketOrder(symbol, -quantity)
                            
                remove_item = item
                
            elif item.holding_period == 0:
                open_symbol_q = []
                
                for symbol, quantity in item.symbol_q:
                    if symbol in data and data[symbol]:
                        self.MarketOrder(symbol, quantity)
                        open_symbol_q.append((symbol, quantity))
                
                # Only opened orders will be closed        
                item.symbol_q = open_symbol_q
                
            item.holding_period += 1
            
        # We need to remove closed part of portfolio after loop. Otherwise it will miss one item in self.managed_queue.
        if remove_item:
            self.managed_queue.remove(remove_item)
    def Selection(self):
        self.selection_flag = True
class RebalanceQueueItem():
    def __init__(self, symbol_q):
        # symbol/quantity collections
        self.symbol_q = symbol_q  
        self.holding_period = 0
# 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 的更多信息

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

继续阅读