Quant BuffetRelax, Not Over Thinking

Combined Value and Profitability in US and Chinese Equities

Log in to collect

Academic paper

Is Value Strategy Still Alive? Evidence from the Chinese A-Share Market

AuthorsFrank Yulin Feng; Wenjin Kang; Shuyan Liu; Huiping Zhang; Zhang Kang

Institute
  • 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)