“该策略投资于全球所有国家指数(ACWI)中具有可用碳强度和市值数据的股票,数据来源于S&P Trucost。首先,计算每家公司每百万美元收入的二氧化碳当量的碳排放强度,涵盖范围1至范围3的排放量。策略为市值加权,每年再平衡以减少碳足迹。通过升序排序碳强度,保留累积市场权重达到90%的低排放公司,并剔除高排放公司,最后调整权重以保持投资组合的行业和地区暴露与基准相似。”
资产类别:股票 | 地区:全球 | 频率:每年 | 市场:股票 | 关键词:基准投资组合,碳足迹,ACWI,全球股票市场
策略概述
投资范围包括ACWI(全球所有国家指数)中具有可用碳强度测量值和市值数据的股票。排放数据来自S&P Trucost。第一步,计算每家公司碳排放强度,单位为每百万美元收入的二氧化碳当量(即总碳排放量除以公司总收入),碳强度数据涵盖范围1至范围3的排放量。
<策略实施步骤>
- 投资组合再平衡:该策略为市值加权,并每年进行再平衡,以确保投资组合持续减少碳足迹。
- 碳强度计算:计算每家公司单位收入的碳排放量。碳强度的计算考虑了公司范围1(直接排放)、范围2(购买能源产生的间接排放)以及范围3(供应链和产品使用阶段的排放)排放量。
- 股票排序与筛选:根据碳排放强度的升序对股票进行排序。然后,按市场权重(公司市值除以所有股票总市值)累积计算,直至累积市场权重达到总市场权重的90%(或其他标准,如75%、99%)。保留占总市值90%的股票,剔除其余10%的高排放公司。
- 权重调整:按市值对保留下来的股票进行再加权,确保投资组合中的所有资金均被重新分配。该步骤旨在创建一个市值加权且碳排放强度较低的投资组合,同时保持行业和地区暴露与原基准类似。
- 投资组合示例:假设有三只股票,市值分别为500万、400万和100万美元,碳排放强度分别为(2,1,3)。首先包含碳排放最少的第二只股票,占总市值的40%。接着加入第一只股票,总市值达到90%,第三只股票被剔除,重新调整前两只股票的权重以实现完全投资。
策略合理性
该策略的有效性来源于碳强度分布的右偏特性。少数公司具有极高的碳排放强度,这些公司是极端的高排放企业。通过剔除这些少数高污染公司,可以显著降低整体投资组合的碳足迹,同时对基准表现的影响较小。无论这些高排放公司的剔除是否能提升回报率,仅剔除约1%的公司(按市场市值计)就可以显著减少组合的碳排放强度,而不显著影响整体的回报率和风险水平。
论文来源
Building Benchmarks Portfolios with Decreasing Carbon Footprints [点击浏览原文]
- Eric Jondeau, 洛桑大学 – 商业与经济学院 (HEC Lausanne)
- Benoît Mojon, 瑞士金融研究院 (SFI)
- Luiz A. Pereira da Silva, 国际清算银行 (BIS)
<摘要>
本文构建了逐步减少碳足迹的投资组合,供被动投资者作为与《巴黎协定》一致的新基准使用,同时保持与传统基准相同的风险调整回报率。由于公司的碳强度分布极为偏斜,剔除一小部分高污染公司即可大幅降低公司股票组合的碳足迹。我们识别了全球范围内的最差污染企业,将其排除在组合之外,并重新分配资金,以保持行业和地区暴露与传统基准相似。这种方法限制了对新兴市场企业的撤资,同时确保区域暴露的稳定性。结果表明,通过连续剔除高达11%的公司,可以在10年内将组合的碳足迹减少64%


回测表现
| 年化收益率 | 8.2% |
| 波动率 | 16.5% |
| Beta | 0.924 |
| 夏普比率 | 0.5 |
| 索提诺比率 | 0.545 |
| 最大回撤 | N/A |
| 胜率 | 90% |
完整python代码
from AlgorithmImports import *
from typing import List, Dict
# endregion
class BenchmarksPortfolioswithDecreasingCarbonFootprints(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetCash(100_000)
self.leverage: int = 3
self.quantile: int = 3
self.portfolio_threshold: float = 0.75
self.data: Dict[int, Dict[str, float]] = {}
# DJI stocks
self.tickers: List[str] = [
'AXP', 'AMGN', 'AAPL', 'BA', 'CAT', 'CSCO', 'CVX', 'GS', 'HD', 'HON',
'IBM', 'INTC', 'JNJ', 'KO', 'JPM', 'MCD', 'MMM', 'MRK', 'MSFT', 'NKE',
'PG', 'TRV', 'UNH', 'CRM', 'VZ', 'V', 'WBA', 'WMT', 'DIS', 'DOW'
]
for ticker in self.tickers:
data: Equity = self.AddEquity(ticker, Resolution.Daily)
data.SetLeverage(self.leverage)
# Source: https://esg.exerica.com/Company?Name=Apple
ghg_emissions: str = self.Download('data.quantpedia.com/backtesting_data/economic/GHG_emissions.csv')
lines: List[str] = ghg_emissions.split('\r\n')
for line in lines[1:]: # skip first comment lines
if line == '':
continue
line_split: List[str] = line.split(';')
year: str = line_split[0]
if year not in self.data:
self.data[year] = {}
# N stocks -> n*4 properties
for i in range(1, len(line_split), 4):
# parse GHG emissions info
if line_split[i] not in self.data[year]:
self.data[year][line_split[i]] = sum( float(line_split[x]) for x in range(i+1, i+4) if line_split[x] != '' )
self.rebalance_month: int = 7
self.current_month: int = -1
def OnData(self, slice: Slice) -> None:
if self.Time.month == self.current_month:
return
self.current_month = self.Time.month
if self.Time.month != self.rebalance_month:
return
if str(self.Time.year - 1) not in list(self.data.keys()):
return
# calculate emission intensity
emission_intensity: Dict[Symbol, float] = {
self.Symbol(ticker) : self.data[str(self.Time.year - 1)][ticker] / self.Securities[ticker].Fundamentals.FinancialStatements.IncomeStatement.TotalRevenue.TwelveMonths \
for ticker in self.tickers if self.data[str(self.Time.year - 1)][ticker] != 0 and self.Securities[ticker].Fundamentals.HasFundamentalData\
and not np.isnan(self.Securities[ticker].Fundamentals.FinancialStatements.IncomeStatement.TotalRevenue.TwelveMonths) \
and self.Securities[ticker].Fundamentals.FinancialStatements.IncomeStatement.TotalRevenue.TwelveMonths != 0
}
weight: Dict[Symbol, float] = {}
final_portfolio: List[Symbol] = []
portfolio_percentage: float = 0.
# sort and divide portfolio
if len(emission_intensity) != 0:
sorted_emissions:List[Symbol] = sorted(emission_intensity, key=emission_intensity.get)
# calculate weights based on marketcap
mc_sum:float = sum(list(map(lambda symbol: self.Securities[symbol].Fundamentals.MarketCap, sorted_emissions)))
for symbol in sorted_emissions:
w: float = self.Securities[symbol].Fundamentals.MarketCap / mc_sum
portfolio_percentage += w
if portfolio_percentage <= self.portfolio_threshold:
final_portfolio.append(symbol)
if len(final_portfolio) != 0:
mc_sum: float = sum(list(map(lambda symbol: self.Securities[symbol].Fundamentals.MarketCap, final_portfolio)))
for symbol in final_portfolio:
weight[symbol] = self.Securities[symbol].Fundamentals.MarketCap / mc_sum
# trade execution
portfolio:List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in weight.items() if symbol in slice and slice[symbol]]
self.SetHoldings(portfolio, True)
