
“该策略交易中国股票,形成基于规模和市盈率的价值加权投资组合,将回报计算为小盘股和大盘股之间的差异,并每月重新平衡以实现系统性绩效。”
资产类别: 股票 | 地区: 中国 | 周期: 每月 | 市场: 股票 | 关键词: 规模因子、中国
I. 策略概要
该策略交易万得信息股份有限公司(WIND)的中国股票,重点关注市值最大的70%的股票。这些股票根据市值中位数分为两个规模组:小盘股(S)和大盘股(B)。每个规模组进一步分为三个EP类别:价值(前30%)、中等(中间40%)和成长(后30%)。这创建了六个价值加权投资组合:S/V、S/M、S/G、B/V、B/M和B/G,按已发行A股的市值加权。回报计算为S投资组合的平均值减去B投资组合的平均值,每月重新平衡。
II. 策略合理性
规模因子捕捉与企业规模相关的股票风险和回报差异。在中国,小盘股通常反映的是与IPO过程相关的价值,而非潜在的业务基本面。中国的IPO市场受到严格监管,需求旺盛但审批能力有限,促使私营企业采用反向并购。这些公司通过收购已上市的“壳”公司来快速上市。与其他市场不同,中国最小的股票经常被视为壳公司目标,这使得它们在规模相关的业务风险方面代表性不足。因此,策略排除了最小的30%的股票,使其与美国和其他发达市场保持一致,确保与规模因子的相关性。
III. 来源论文
Size and Value in China [点击查看论文]
刘家男、Robert F. Tambaugh 和 袁宇。明石投资管理;香港大学。宾夕法尼亚大学沃顿商学院;国家经济研究局 (NBER)。上海明石投资有限公司;宾夕法尼亚大学沃顿金融机构中心
<摘要>
我们构建了中国的规模和价值因子。规模因子排除了最小的30%的公司,这些公司作为反向并购中潜在的壳公司而具有显著价值,从而规避了严格的IPO限制。价值因子基于市盈率,该比率在捕捉所有中国价值效应方面取代了账面市值比。我们的三因子模型显著优于仅通过在中国复制Fama和French(1993)程序形成的模型。与该模型不同的是,该模型在市盈率因子上留下了17%的年化阿尔法,而我们的模型解释了大多数已报告的中国异常现象,包括盈利能力和波动率异常。


IV. 回测表现
| 年化回报 | 13.08% |
| 波动率 | 15.66% |
| β值 | 0.06 |
| 夏普比率 | 0.83 |
| 索提诺比率 | -0.289 |
| 最大回撤 | N/A |
| 胜率 | 56% |
V. 完整的 Python 代码
from AlgorithmImports import *
import numpy as np
from numpy import isnan
from typing import List, Dict
#endregion
class SizeFactorInChina(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2005, 1, 1)
self.SetCash(100000)
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] = {}
symbol: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.selection_flag: bool = False
self.settings.daily_precise_end_time = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Schedule.On(self.DateRules.MonthStart(symbol), self.TimeRules.AfterMarketOpen(symbol), self.Selection)
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]
if len(earnings_price_ratio) == 0:
return Universe.Unchanged
# 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 B/V, B/M, B/G, S/M, S/G, and S/V by intersection
B_V: List[Symbol] = [x for x in B if x in V]
B_M: List[Symbol] = [x for x in B if x in M]
B_G: List[Symbol] = [x for x in B if x in G]
long: List[Symbol] = B_V + B_M + B_G
S_M: List[Symbol] = [x for x in S if x in M]
S_G: List[Symbol] = [x for x in S if x in G]
S_V: List[Symbol] = [x for x in S if x in V]
short: List[Symbol] = S_M + S_G + S_V
# go long B/V, B/M, and B/G
total_market_cap_B_V: float = sum(market_cap[x] for x in B_V)
total_market_cap_B_M: float = sum(market_cap[x] for x in B_M)
total_market_cap_B_G: float = sum(market_cap[x] for x in B_G)
for symbol in long:
if symbol in B_V:
self.weight[symbol] = market_cap[symbol] / total_market_cap_B_V / 3
elif symbol in B_M:
self.weight[symbol] = market_cap[symbol] / total_market_cap_B_M / 3
else:
self.weight[symbol] = market_cap[symbol] / total_market_cap_B_G / 3
# go short S/M, S/G, and S/V
total_market_cap_S_M: float = sum(market_cap[x] for x in S_M)
total_market_cap_S_G: float = sum(market_cap[x] for x in S_G)
total_market_cap_S_V: float = sum(market_cap[x] for x in S_V)
for symbol in short:
if symbol in S_M:
self.weight[symbol] = -market_cap[symbol] / total_market_cap_S_M / 3
elif symbol in S_G:
self.weight[symbol] = -market_cap[symbol] / total_market_cap_S_G / 3
else:
self.weight[symbol] = -market_cap[symbol] / total_market_cap_S_V / 3
return long + short
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 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"))