Combined Value and Profitability in US and Chinese Equities
Log in to collectAcademic paper
Is Value Strategy Still Alive? Evidence from the Chinese A-Share Market
Frank Yulin Feng; Wenjin Kang; Shuyan Liu; Huiping Zhang; Zhang Kang
- Shanghai University of Finance and Economics
- MOUniversity of Macau
- ?Faculty of Business Administration, University of Macau
- James Cook University
- ?James Cook University - College of Business, Law and Governance,
Strategy in a nutshell
Constructs a value-profit composite factor using Chinese A-share stocks and long-only US value (HML) and profitability (RMW) factors; invests in top-ranked value/profitable stocks across regions, portfolios are value-weighted and rebalanced monthly.
Economic rationale
Combines valuation, profitability, and geographic diversification to capture mispriced or high-quality firms, targeting stocks likely to generate higher expected returns as prices converge toward intrinsic value.
Backtest performance
Annualised return13.22%
Volatility15.93%
Beta0.281
Sharpe ratio0.83
Maximum drawdown55.29%
Win rate69%
Full Python code
from AlgorithmImports import *
from data_tools import CustomFeeModel, ChineseStocks, ChineseIncomeStatement, SymbolData, FamaFrench, ChineseBalanceSheet
from typing import List, Dict
# endregion
class CombinedValueandProfitabilityinUSandChineseEquities(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 1, 1) # Chinese data starts in 2015
self.SetCash(100000)
self.leverage:int = 5
self.chinese_portfolio_weight:float = 0.5
self.us_portfolio_part_weight:float = 0.25
self.recent_month:int = -1
self.min_stocks:int = 7
self.percent_stocks_to_trade:float = 0.3
self.data:Dict[Symbol, SymbolData] = {}
self.top_size_symbol_count:int = 100
ticker_file_str:str = self.Download('data.quantpedia.com/backtesting_data/equity/chinese_stocks/large_cap_500.csv')
tickers:List[str] = ticker_file_str.split('\r\n')[:self.top_size_symbol_count]
for t in tickers:
data = self.AddData(ChineseStocks, t, Resolution.Daily)
data.SetFeeModel(CustomFeeModel())
data.SetLeverage(self.leverage)
stock_symbol:Symbol = data.Symbol
data = self.AddData(ChineseIncomeStatement, t, Resolution.Daily)
income_statement_symbol:Symbol = data.Symbol
data = self.AddData(ChineseBalanceSheet, t, Resolution.Daily)
balance_sheet_symbol:Symbol = data.Symbol
self.data[stock_symbol] = SymbolData(income_statement_symbol, balance_sheet_symbol)
data = self.AddData(FamaFrench, 'fama_french_6_book_to_market_daily_price', Resolution.Daily)
data.SetFeeModel(CustomFeeModel())
self.HML = data.Symbol
data = self.AddData(FamaFrench, 'fama_french_6_profitability_daily_price', Resolution.Daily)
data.SetFeeModel(CustomFeeModel())
self.RMW = data.Symbol
def OnData(self, data: Slice):
curr_date:datetime.date = self.Time.date()
# store daily data
for symbol, symbol_data in self.data.items():
income_statement_symbol:Symbol = symbol_data.get_income_statement_symbol()
balance_sheet_symbol:Symbol = symbol_data.get_balance_sheet_symbol()
if data.ContainsKey(symbol):
price_data:Dict[str, str] = data[symbol].GetProperty('price_data')
# valid price data
if data[symbol].Value != 0. and price_data:
mc:float = float(price_data['marketValue'])
symbol_data.set_market_equity(mc)
if data.ContainsKey(income_statement_symbol):
# update operating, gross and net profit from income statement
operating_profit:float = float(data[income_statement_symbol].GetProperty('operating_profit'))
gross_profit:float = float(data[income_statement_symbol].GetProperty('gross_profit'))
net_profit:float = float(data[income_statement_symbol].GetProperty('net_profit'))
symbol_data.set_operating_profit(operating_profit)
symbol_data.set_gross_profit(gross_profit)
symbol_data.set_net_profit(net_profit)
if data.ContainsKey(balance_sheet_symbol):
# update total assets and total liabilities from balance sheet
total_assets:float = float(data[balance_sheet_symbol].GetProperty('total_assets'))
total_liabilities:float = float(data[balance_sheet_symbol].GetProperty('total_liabilities'))
symbol_data.set_total_assets(total_assets)
symbol_data.set_total_liabilities(total_liabilities)
# rebalance monthly
if self.recent_month == self.Time.month:
return
self.recent_month = self.Time.month
symbols_with_measures:List[Symbol] = []
measures:Dict[str, Dict[Symbol, float]] = {
'OP_to_ME': {},
'GP_to_ME': {},
'NP_to_ME': {},
'GP_to_TA': {},
'OP_to_BE': {},
'NP_to_BE': {},
}
for symbol, symbol_data in self.data.items():
if not symbol_data.is_ready() or symbol not in data or not data[symbol] or \
data[symbol].Value == 0 or not self.Securities[symbol].IsTradable:
continue
# calculate required mesaures
measures['OP_to_ME'][symbol] = symbol_data.get_operating_profit_to_market_equity()
measures['GP_to_ME'][symbol] = symbol_data.get_gross_profit_to_market_equity()
measures['NP_to_ME'][symbol] = symbol_data.get_net_profit_to_market_equity()
measures['GP_to_TA'][symbol] = symbol_data.get_gross_profit_to_total_assets()
measures['OP_to_BE'][symbol] = symbol_data.get_operating_profit_to_book_equity()
measures['NP_to_BE'][symbol] = symbol_data.get_net_profit_to_book_equity()
symbols_with_measures.append(symbol)
# make sure there are enough stocks with measures for selection
if len(symbols_with_measures) < self.min_stocks:
self.Liquidate()
return
# sort dictionary of measures by measure values
measures_sorted:List[List[Symbol]] = []
for _, value_by_symbol in measures.items():
sorted_measure:List[Symbol] = [x[0] for x in sorted(value_by_symbol.items(), key=lambda item: item[1])]
measures_sorted.append(sorted_measure)
# calculate total rank based on stock orders in sorted measures
total_rank:Dict[Symbol, float] = {}
for symbol in symbols_with_measures:
rank:int = sum(list(map(lambda sorted_measure: sorted_measure.index(symbol), measures_sorted)))
total_rank[symbol] = rank
# sort by rank select long and trade
sorted_by_rank:List[Symbol] = [x[0] for x in sorted(total_rank.items(), key=lambda item: item[1])]
long_leg:List[Symbol] = sorted_by_rank[-int(len(sorted_by_rank) * self.percent_stocks_to_trade):]
# trade execution
invested:List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested and x.Key not in [self.HML, self.RMW]]
for symbol in invested:
if symbol not in long_leg:
self.Liquidate(symbol)
long_length:int = len(long_leg)
for symbol in long_leg:
self.SetHoldings(symbol, (1 / long_length) * self.chinese_portfolio_weight)
if not self.Securities[self.HML].Invested:
self.SetHoldings(self.HML, self.us_portfolio_part_weight)
if not self.Securities[self.RMW].Invested:
self.SetHoldings(self.RMW, self.us_portfolio_part_weight)