The strategy trades Chinese stocks, constructing size and EP-based value-weighted portfolios, calculating a value factor by combining high-EP and low-EP groups, rebalanced monthly for systematic returns.

I. STRATEGY IN A NUTSHELL

The strategy trades Chinese stocks using earnings-price (EP) value and size factors, constructing six value-weighted portfolios and rebalancing monthly to capture value-driven returns.

II. ECONOMIC RATIONALE

EP best explains cross-sectional returns in China, outperforming other valuation ratios; the EP-based value factor captures the value effect more effectively than book-to-market measures.

III. SOURCE PAPER

Size and Value in China [Click to Open PDF]

.<Abstract>

We construct size and value factors in China. The size factor excludes the smallest 30% of firms, which are companies valued significantly as potential shells in reverse mergers that circumvent tight IPO constraints. The value factor is based on the earnings-price ratio, which subsumes the book-to-market ratio in capturing all Chinese value effects. Our three-factor model strongly dominates a model formed by just replicating the Fama and French (1993) procedure in China. Unlike that model, which leaves a 17% annual alpha on the earnings-price factor, our model explains most reported Chinese anomalies, including profitability and volatility anomalies.

IV. BACKTEST PERFORMANCE

Annualised Return14.57%
Volatility12.99%
Beta-0.019
Sharpe Ratio1.12
Sortino Ratio-0.094
Maximum DrawdownN/A
Win Rate46%

V. FULL PYTHON CODE

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

VI. Backtest Performance

Leave a Reply

Discover more from Quant Buffet

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

Continue reading