
“该策略交易中国股票,构建基于规模和市盈率的价值加权投资组合,通过结合高市盈率和低市盈率组来计算价值因子,并每月重新平衡以实现系统性回报。”
资产类别: 股票 | 地区: 中国 | 周期: 每月 | 市场: 股票 | 关键词: 价值因子、中国
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 [点击查看论文]
- 刘家男、Robert F. Tambaugh 和 袁宇。明石投资管理;香港大学。宾夕法尼亚大学沃顿商学院;国家经济研究局 (NBER)。上海明石投资有限公司;宾夕法尼亚大学沃顿金融机构中心
<摘要>
我们构建了中国的规模和价值因子。规模因子排除了最小的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"))