“该策略交易中国股票,形成基于规模和市盈率的价值加权投资组合,将回报计算为小盘股和大盘股之间的差异,并每月重新平衡以实现系统性绩效。”

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"))

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读