“该策略交易中国股票,构建基于规模和市盈率的价值加权投资组合,通过结合高市盈率和低市盈率组来计算价值因子,并每月重新平衡以实现系统性回报。”

I. 策略概要

该策略重点关注万得信息股份有限公司(WIND)的中国股票,排除了最小的30%的股票。价值因子使用EP(市盈率倒数)构建,遵循Fama和French(1993)的方法。剩余股票根据市值中位数分为两个规模组:小盘股(S)和大盘股(B),以及三个EP组:价值(前30%)、中等(中间40%)和成长(后30%)。这创建了六个价值加权投资组合:S/V、S/M、S/G、B/V、B/M和B/G。投资组合按市值加权,包括不可交易股份。价值因子计算为S/V和B/V的平均值减去S/G和B/G的平均值,每月重新平衡。

II. 策略合理性

中国的价值效应最好通过市盈率(EP)来捕捉,而不是其他估值比率。该研究分析了哪个变量最能捕捉平均股票回报的横截面变化。在所有候选估值比率(包括EP、账面市值比(BM)、资产市值比和现金流价格比)中进行了一场赛马。EP在中国市场的优越性至少通过两种方式得到证明。首先,在一项与Fama和French(1992)并行的研究中,横截面回归显示EP在解释平均股票回报方面涵盖了包括BM在内的其他估值比率。其次,其EP基础的价值因子三因子模型CH-3,优于其BM基础的价值因子替代FF-3模型。在正面模型比较中,CH-3对FF-3中的规模和价值因子都进行了定价,而FF-3则对CH-3中的规模和价值因子都没有进行定价。

III. 来源论文

Size and Value in China [点击查看论文]

<摘要>

我们构建了中国的规模和价值因子。规模因子排除了最小的30%的公司,这些公司作为反向并购中潜在的壳公司而具有显著价值,从而规避了严格的IPO限制。价值因子基于市盈率,该比率在捕捉所有中国价值效应方面取代了账面市值比。我们的三因子模型显著优于仅通过在中国复制Fama和French(1993)程序形成的模型。与该模型不同的是,该模型在市盈率因子上留下了17%的年化阿尔法,而我们的模型解释了大多数已报告的中国异常现象,包括盈利能力和波动率异常。

IV. 回测表现

年化回报14.57%
波动率12.99%
β值-0.019
夏普比率1.12
索提诺比率-0.094
最大回撤N/A
胜率46%

V. 完整的 Python 代码

from AlgorithmImports import *
import numpy as np
from numpy import isnan
from typing import List, Dict
#endregion
class ValueFactorinChina(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100_000)
        
        self.leverage: int = 5
        self.market_cap_portion: float = 0.3
        self.quantile: int = 3
        self.traded_portion: float = 0.2
        self.weight: Dict[Symbol, float] = {}
        
        market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        
        self.selection_flag: bool = False
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Schedule.On(self.DateRules.MonthStart(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.SetFeeModel(CustomFeeModel())
            security.SetLeverage(self.leverage)
    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.MarketCap != 0 
            and x.CompanyReference.BusinessCountryID == 'CHN'
            and not isnan(x.ValuationRatios.PERatio) and x.ValuationRatios.PERatio != 0 
        ]
        # exclude 30% of lowest stocks by MarketCap
        selected = sorted(selected, key = lambda x: x.MarketCap)[int(len(selected) * self.market_cap_portion):]
        market_cap: Dict[Symbol, float] = {}
        earnings_price_ratio: Dict[Symbol, float] = {}
        
        for stock in selected:
            symbol: Symbol = stock.Symbol
                
            market_cap[symbol] = stock.MarketCap
            earnings_price_ratio[symbol] = 1 / stock.ValuationRatios.PERatio
            
        median_market_value: float = np.median([market_cap[x] for x in market_cap])
        
        # according to median split into BIG and SMALL
        B: List[Symbol] = [x for x in market_cap if market_cap[x] >= median_market_value]
        S: List[Symbol] = [x for x in market_cap if market_cap[x] < median_market_value]
        
        # split into three groups according to earnings_price_ratio
        quantile: int = int(len(earnings_price_ratio) / self.quantile)
        sorted_by_earnings_price_ratio: List[Symbol] = [x[0] for x in sorted(earnings_price_ratio.items(), key=lambda item: item[1])]
        
        V: List[Symbol] = sorted_by_earnings_price_ratio[-quantile:]
        M: List[Symbol] = sorted_by_earnings_price_ratio[quantile:-quantile]
        G: List[Symbol] = sorted_by_earnings_price_ratio[:quantile]
        # create  S/V, B/V, B/G, and S/G by intersection
        S_V: List[Symbol] = [x for x in S if x in V]
        B_V: List[Symbol] = [x for x in B if x in V]
        long: List[Symbol] = S_V + B_V
        
        B_G: List[Symbol] = [x for x in B if x in G]
        S_G: List[Symbol] = [x for x in S if x in G]
        short: List[Symbol] = B_G + S_G
        
        # long S/V and B/V 
        total_market_cap_S_V: float = sum(market_cap[x] for x in S_V)
        total_market_cap_B_V: float = sum(market_cap[x] for x in B_V)
        
        for symbol in long:
            if symbol in S_V:
                self.weight[symbol] = market_cap[symbol] / total_market_cap_S_V / 2
            else:
                self.weight[symbol] = market_cap[symbol] / total_market_cap_B_V / 2
        
        # short B/G and S/G
        total_market_cap_B_G: float = sum(market_cap[x] for x in B_G)
        total_market_cap_S_G: float = sum(market_cap[x] for x in S_G)
        
        for symbol in short:
            if symbol in B_G:
                self.weight[symbol] = -market_cap[symbol] / total_market_cap_B_G / 2
            else:
                self.weight[symbol] = -market_cap[symbol] / total_market_cap_S_G / 2
        
        return list(self.weight.keys())
    def OnData(self, data: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        # trade execution
        portfolio: List[PortfolioTarget] = [
            PortfolioTarget(symbol, w * self.traded_portion) for symbol, w in self.weight.items() if symbol in data and data[symbol]
        ]
        self.SetHoldings(portfolio, True)
        self.weight.clear()
        
    def Selection(self) -> None:
        self.selection_flag = True
        
# custom fee mode
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读