该策略投资于中国A股市场的非金融公司,剔除账面价值为负或零的公司。排序指标为异常资产增长率(AAG),计算方式为某财年上半年资产增长率减去前两个半年的平均值。根据AAG值将股票分为十组,做多AAG值最高的,做空AAG值最低的。投资组合按价值加权,每六个月调整一次。

策略概述

投资领域由在中国A股市场上市的非金融公司构成,剔除账面价值为负或零的公司。排序指标为异常资产增长率(AAG),其计算公式在论文第10页:某一财年上半年的资产增长率减去之前两个半年期资产增长率的平均值。根据AAG值将股票分为十组,做多AAG值最高的股票,做空AAG值最低的股票。投资组合按价值加权,并每六个月重新调整(持仓期为5月至10月,11月至次年4月)。

策略合理性

作者提供了一种颇具说服力的解释,统一了已知的其他市场上的看似矛盾的现象与这些新发现。发展中市场通常表现不同于发达市场,中国市场就是这种情况。与美国不同,在美国资本回报率是递减的,而在中国则不是。因此,根据本研究,企业投资与未来股票回报呈正相关关系。许多因素可能在其中发挥作用。例如,将资本投资于实体资产可以使公司在快速发展的经济中拥有更高效的机械设备和更高的工作效率。

论文来源

Does Higher Investments Necessarily Reduce Stock Returns? [点击浏览原文]

<摘要>

先前的研究发现,企业投资的增加往往会负面预测公司表现和股票回报。使用中国A股市场的数据,我们发现大幅增加投资的公司具有更高的后续股票回报,而非更低。在控制规模、价值、动量和换手率等重要价格因素后,这种效应仍然存在。进一步分析表明,这种效应在大公司、低账面市值比公司、盈利公司和高增长行业中更为明显。我们提供了一个统一的解释:投资与股票回报的关系取决于资本回报率是递减还是递增的。我们发现,中国过去二十年中资本回报率呈上升趋势,这意味着更多的投资会带来更高的利润,从而导致更高的股票回报。

回测表现

年化收益率10.95%
波动率9.38%
Beta-0.029
夏普比率1.17
索提诺比率-0.319
最大回撤N/A
胜率45%

完整python代码

from AlgorithmImports import *
from numpy import isnan

class InvestmentEffectInChina(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        
        self.data:Dict[Symbol, SymbolData] = {}
        self.weight:Dict[Symbol, float] = {}
        
        self.percentile_size:int = 5
        self.portfolio_size:float = 0.1
        
        self.TA_period:int = 2
        self.TA_growth_period:int = 2
        self.leverage:int = 5
        
        self.market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        
        self.rebalance_flag:bool = False
        self.selection_flag:bool = False
        self.rebalance_months:List[int] = [5, 11]
        self.selection_months:List[int] = [2, 5, 8, 11]
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.Schedule.On(self.DateRules.MonthStart(self.market), self.TimeRules.BeforeMarketClose(self.market), 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]) -> None:
        if not self.selection_flag:
            return Universe.Unchanged
        self.selection_flag = False
        
        # filter stocks, which symbols isn't SPY equity symbol and has fundamental data
        selected:List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Symbol != self.market and x.CompanyReference.BusinessCountryID == 'CHN' and \
            not isnan(x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths) and x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths != 0 and x.MarketCap != 0
            ]

        AAG:Dict[Fundamental, float] = {}
        
        for stock in selected:
            symbol:Symbol = stock.Symbol
            total_assets:float = stock.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths
           
            if symbol not in self.data or symbol not in self.previous_fine:
                self.data[symbol] = SymbolData(self.TA_period, self.TA_growth_period)
            
            symbol_obj:SymbolData = self.data[symbol]  
            symbol_obj.update_TA(total_assets)
            
            # update Total Assets growth only in May and November
            if self.Time.month in self.rebalance_months:
                if symbol_obj.TA_growths_ready() and symbol_obj.TA_ready():
                    # calculate stock's AAG
                    curr_TA_growth:float = symbol_obj.calculate_TA_growth()
                    prev_TA_growths:List[float] = [x for x in symbol_obj._total_assets_growths]
                    
                    AAG_value:float = curr_TA_growth - np.mean(prev_TA_growths)
                    
                    AAG[stock] = AAG_value
                    
                    # update stock's total assets growth 
                    symbol_obj.update_TA_growths(curr_TA_growth)
                    
                elif symbol_obj.TA_ready():
                    # update stock's total assets growth
                    curr_TA_growth:float = symbol_obj.calculate_TA_growth()
                    symbol_obj.update_TA_growths(curr_TA_growth)
            
        self.previous_fine = list(map(lambda stock: stock.Symbol, selected))
          
        # make sure there are enough data for selection
        if len(AAG) < self.percentile_size:
            return Universe.Unchanged
            
        percentile:int = int(len(AAG) / self.percentile_size)
        sorted_by_AAG:List[Fundamental] = [x[0] for x in sorted(AAG.items(), key=lambda item: item[1])]
        
        # long the highest
        long = sorted_by_AAG[-percentile:]

        # short the lowest
        short = sorted_by_AAG[:percentile]
        
        # calculate total capitalization for long and short part
        for i, portfolio in enumerate([long, short]):
            mc_sum:float = sum([x.MarketCap for x in portfolio])
            for stock in portfolio:
                self.weight[symbol] = ((-1) ** i) * stock.MarketCap / mc_sum * self.portfolio_size
        
        return list(self.weight.keys()) 
        
    def OnData(self, data: Slice) -> None:
        # rebalancing only in May and November
        if not self.rebalance_flag:
            return
        self.rebalance_flag = False
        
        # trade execution
        portfolio:List[PortfolioTarget] = [PortfolioTarget(symbol, w) 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:
        # perform selection on February, May, August and November
        if self.Time.month in self.selection_months:
            self.selection_flag = True
            
        # perform rebalance in April, May, October and November 
        if self.Time.month in self.rebalance_months:
            self.rebalance_flag = True
        
class SymbolData():
    def __init__(self, TA_period: int, TA_growth_period: int) -> None:
        self._total_assets:RollingWindow = RollingWindow[float](TA_period)
        self._total_assets_growths:RollingWindow = RollingWindow[float](TA_growth_period)
        
    def update_TA(self, total_assets: float) -> None:
        self._total_assets.Add(total_assets)
        
    def update_TA_growths(self, total_assets_growths) -> None:
        self._total_assets_growths.Add(total_assets_growths)
        
    def calculate_TA_growth(self) -> float:
        total_assets:List[float] = list(self._total_assets)
        return (total_assets[0] - total_assets[-1]) / total_assets[-1] 

    def TA_ready(self) -> bool:
        return self._total_assets.IsReady
        
    def TA_growths_ready(self) -> bool:
        return self._total_assets_growths.IsReady
        
# Custom fee model
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading