“该策略选择ESG评级和员工满意度最高的美国公司进行多头头寸,选择排名后四分之一的股票进行空头头寸。投资组合采用价值加权,每月重新平衡。”

I. 策略概要

该投资范围包括同时拥有MSCI ESG评级和Glassdoor员工满意度数据的美国公司。股票根据这些指标被分为四分位数。ESG和员工满意度均排名前四分之一的公司被选入多头投资组合,而两者均排名后四分之一的公司则被选入空头投资组合。投资组合采用价值加权,每月重新平衡。

II. 策略合理性

该研究发现,使用员工满意度数据作为信号,在多空投资组合中产生了每年2.44%的价值加权阿尔法,而单独的ESG并未产生有意义的阿尔法。该策略的积极结果主要由员工满意度驱动,ESG增强了这些结果。研究强调了人力资本在现代企业中日益增长的重要性,因为员工满意度已被证明可以预测股票回报和公司价值。作者强调,虽然高ESG和员工满意度会带来更好的业绩,但他们并不声称ESG直接导致员工满意度或提升公司价值,而是满意度对于ESG对股东价值的积极影响至关重要。

III. 来源论文

Corporate Sustainability and Stock Returns: Evidence from Employee Satisfaction [点击查看论文]

<摘要>

公司管理者正面临越来越大的外部压力,要求他们将公司资源分配给环境、社会和治理(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"))

发表评论

了解 Quant Buffet 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读