
“该策略选择ESG评级和员工满意度最高的美国公司进行多头头寸,选择排名后四分之一的股票进行空头头寸。投资组合采用价值加权,每月重新平衡。”
资产类别: 股票 | 地区: 美国 | 周期: 每月 | 市场: 股票 | 关键词: 员工满意度、ESG
I. 策略概要
该投资范围包括同时拥有MSCI ESG评级和Glassdoor员工满意度数据的美国公司。股票根据这些指标被分为四分位数。ESG和员工满意度均排名前四分之一的公司被选入多头投资组合,而两者均排名后四分之一的公司则被选入空头投资组合。投资组合采用价值加权,每月重新平衡。
II. 策略合理性
该研究发现,使用员工满意度数据作为信号,在多空投资组合中产生了每年2.44%的价值加权阿尔法,而单独的ESG并未产生有意义的阿尔法。该策略的积极结果主要由员工满意度驱动,ESG增强了这些结果。研究强调了人力资本在现代企业中日益增长的重要性,因为员工满意度已被证明可以预测股票回报和公司价值。作者强调,虽然高ESG和员工满意度会带来更好的业绩,但他们并不声称ESG直接导致员工满意度或提升公司价值,而是满意度对于ESG对股东价值的积极影响至关重要。
III. 来源论文
Corporate Sustainability and Stock Returns: Evidence from Employee Satisfaction [点击查看论文]
- Kyle Welch, Aaron Yoon。乔治华盛顿大学;乔治华盛顿大学会计系;西北大学会计信息与管理系。
<摘要>
公司管理者正面临越来越大的外部压力,要求他们将公司资源分配给环境、社会和治理(ESG)工作。然而,鉴于ESG活动通常被认为与股东价值相悖,管理者可能会发现很难决定他们应该选择哪些项目以及应该投资多少。我们使用MSCI ESG评级和Glassdoor对高级管理人员的员工评级作为公司ESG努力和高管理能力的信号,发现有证据表明高能力管理者以一种提升股东价值的方式将资源分配给ESG。具体来说,我们实施了日历时间投资组合回归设计,发现具有高评级管理者和高ESG的公司比两者评级都低的公司表现出显著更高的未来股票回报。结果对使用不同的固定效应结构以及在面板回归中控制更多协变量都具有鲁棒性。总的来说,结果强调了高级管理者在将资源分配给ESG工作方面的重要性。


IV. 回测表现
| 年化回报 | 5.83% |
| 波动率 | N/A |
| β值 | 0.005 |
| 夏普比率 | N/A |
| 索提诺比率 | -0.479 |
| 最大回撤 | N/A |
| 胜率 | 51% |
V. 完整的 Python 代码
from AlgorithmImports import *
from typing import List, Dict
#endregion
class EmployeeSatisfactionESGAndStockReturns(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2015, 1, 1) # First esg data are from 2016
self.SetCash(100000)
# Switching ratings from letters to number for easier sorting
rating_switcher: Dict[str, int] = {
'AAA': 9,
'AA': 8,
'A': 7,
'BBB': 6,
'BB': 5,
'B': 4,
'CCC': 3,
'CC': 2,
'C': 1,
}
self.leverage: int = 5
self.latest_date_esg: Union[None, datetime.date] = None
self.latest_emp_satisf: Union[None, datetime.date] = None
self.quantile: int = 4
self.weight: Dict[Symbol, float] = {} # Storing stocks holding weights
self.esg_ratings: Dict[str, Dict[datetime.date, int]] = {}
self.employees_satisfaction: Dict[str, Dict[datetime.date, str]] = {}
# Download companies employees satisfaction ratings for each year
csv_string_file: str = self.Download('data.quantpedia.com/backtesting_data/index/EMPLOYEE_SATISFACTION.csv')
lines: List[str] = csv_string_file.split('\r\n')
for line in lines[1:]: # Skip header
line_split: List[str] = line.split(';')
date: datetime.date = datetime.strptime(line_split[0], "%d.%m.%Y").date()
if not self.latest_emp_satisf:
self.latest_emp_satisf = date
if date > self.latest_emp_satisf:
self.latest_emp_satisf = date
company_ticker: str = line_split[1]
rating: str = line_split[2]
# Create dictionary for each company ticker
if company_ticker not in self.employees_satisfaction:
self.employees_satisfaction[company_ticker] = {}
# Under company ticker and year store rating
self.employees_satisfaction[company_ticker][date.year] = rating
# Download companies esg rating
csv_string_file: str = self.Download('data.quantpedia.com/backtesting_data/economic/ESG.csv')
lines: List[str] = csv_string_file.split('\r\n')
# Skip date and get only stocks tickers
header: List[str] = lines[0].split(';')[1:]
# For each company ticker create dictionary in self.esg_ratings
# to store esg ratings under specific dates for specific stocks
for ticker in header:
self.esg_ratings[ticker] = {}
for line in lines[1:]: # Skip header
line_split: List[str] = line.split(';')
date: datetime.date = datetime.strptime(line_split[0], "%d.%m.%Y").date()
self.latest_date_esg = date
ratings: str = line_split[1:] # Exclude date
for i in range(len(ratings)):
# Store stocks rating under specific date, if rating isn't -1
if ratings[i] != '-1':
# Switch rating letters to number
switched_rating: int = rating_switcher[ratings[i]]
# Store number rating under specific date for specific stock
self.esg_ratings[header[i]][date] = switched_rating
self.market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.long_only_flag: bool = True
self.short_market_flag: bool = True
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(self.market), self.TimeRules.AfterMarketOpen(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]) -> List[Symbol]:
# Monthly rebalance
if not self.selection_flag:
return Universe.Unchanged
# check if we still have custom data
if self.Time.date() > self.latest_date_esg or self.Time.date() > self.latest_emp_satisf:
self.Liquidate()
return Universe.Unchanged
# Universe will be created based on stocks tickers in self.employees_satisfaction and self.esg_ratings
selected: List[Fundamental] = [
x for x in fundamental
if x.HasFundamentalData
and x.MarketCap != 0
and x.Symbol.Value in self.employees_satisfaction
and x.Symbol.Value in self.esg_ratings
]
market_cap: Dict[Symbol, float] = {} # Storing stocks market capitalization
esg_ratings: Dict[Symbol, float] = {} # Storing latest stocks esg ratings
employees_satisfaction_ratings: Dict[Symbol, float] = {} # Storing employees satisfaction ratings for each stock
current_year: int = self.Time.year # Getting latest stocks employees satisfaction ratings
current_date: datetime.date = self.Time.date() # Getting latest actual stocks esg ratings
for stock in selected:
symbol: Symbol = stock.Symbol
# Get latest esg rating
esg_rating: float = self.GetRating(self.esg_ratings[symbol.Value], current_date)
# Get latest employees satisfaction rating
employees_satisfaction_rating: float = self.GetRating(self.employees_satisfaction[symbol.Value], current_year)
# Go to next stock if esg rating or employees satisfaction rating is None
if esg_rating == None or employees_satisfaction_rating == None:
continue
# Store market capitalization, esg rating and employees satisfaction rating for current stock
market_cap[stock] = stock.MarketCap
esg_ratings[stock] = esg_rating
employees_satisfaction_ratings[stock] = employees_satisfaction_rating
# Check if there are enough stocks for quartile selection
if len(market_cap) < self.quantile:
return Universe.Unchanged
# Quartile selection
# Firstly sort each dictionary by it's value, then by market cap
esg_top, esg_bottom = self.QuantileSelection(esg_ratings, market_cap, self.quantile)
employees_satisfaction_top, employees_satisfaction_bottom = self.QuantileSelection(employees_satisfaction_ratings, market_cap, self.quantile)
# If a company appears in the top quartile in both sorts, its stock is picked for the long portfolio.
long: List[Symbol] = [x for x in employees_satisfaction_top if x in esg_top]
# When a company appears in the bottom quartile in both sorts, its stock is picked for the short portfolio.
short: List[Symbol] = [x for x in employees_satisfaction_bottom if x in esg_bottom]
# The portfolio is value-weighted.
for i, portfolio in enumerate([long, short]):
mc_sum: float = sum(list(map(lambda stock: stock.MarketCap, portfolio)))
for stock in portfolio:
self.weight[stock.Symbol] = ((-1)**i) * stock.MarketCap / mc_sum
return list(self.weight.keys())
def OnData(self, data: Slice) -> None:
if not self.selection_flag:
return
self.selection_flag = False
# Trade execution
invested: List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in self.weight:
self.Liquidate(symbol)
# Short market, when there are enough stocks for long
if self.short_market_flag and len(self.weight) > 0:
if self.market in data and data[self.market]:
self.SetHoldings(self.market, -1)
# Go only long stocks
if self.long_only_flag:
for symbol, w in self.weight.items():
if w > 0:
if symbol in data and data[symbol]:
self.SetHoldings(symbol, w)
# Go long and short stocks
else:
for symbol, w in self.weight.items():
if symbol in data and data[symbol]:
self.SetHoldings(symbol, w)
self.weight.clear()
def Selection(self) -> None:
self.selection_flag = True
def GetRating(self,
rating_dictionary: Dict[datetime.date, int],
lookup_date_or_year: int) -> float:
rating: Union[None, float] = None
# Go through each date or year (based on picked dictionary) and pick actual latest rating
for date in rating_dictionary:
# Actual latest esg rating is changed if date with rating is later than current date
# Actual latest employees satisfaction rating is changed if year with rating is later than current year
if date <= lookup_date_or_year:
rating = rating_dictionary[date]
return rating
def QuantileSelection(self,
rating_dictionary: Dict[datetime.date, int],
market_cap: Dict[Fundamental, float],
quantile:int) -> float:
quantile: int = int(len(market_cap) / quantile)
# sort dictionary by rating and those with same values sort by thier market cap
sorted_by_rating: List[Fundamental] = [x[0] for x in sorted(rating_dictionary.items(), key=lambda item: (item[1], market_cap[item[0]]))]
# top quartile goes long
top: List[Fundamental] = sorted_by_rating[-quantile:]
# bottom quartile goes short
bottom: List[Fundamental] = sorted_by_rating[:quantile]
return top, bottom
# 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"))