该策略投资于全球所有国家指数(ACWI)中具有可用碳强度和市值数据的股票,数据来源于S&P Trucost。首先,计算每家公司每百万美元收入的二氧化碳当量的碳排放强度,涵盖范围1至范围3的排放量。策略为市值加权,每年再平衡以减少碳足迹。通过升序排序碳强度,保留累积市场权重达到90%的低排放公司,并剔除高排放公司,最后调整权重以保持投资组合的行业和地区暴露与基准相似。

策略概述

投资范围包括ACWI(全球所有国家指数)中具有可用碳强度测量值和市值数据的股票。排放数据来自S&P Trucost。第一步,计算每家公司碳排放强度,单位为每百万美元收入的二氧化碳当量(即总碳排放量除以公司总收入),碳强度数据涵盖范围1至范围3的排放量。

<策略实施步骤>

策略合理性

该策略的有效性来源于碳强度分布的右偏特性。少数公司具有极高的碳排放强度,这些公司是极端的高排放企业。通过剔除这些少数高污染公司,可以显著降低整体投资组合的碳足迹,同时对基准表现的影响较小。无论这些高排放公司的剔除是否能提升回报率,仅剔除约1%的公司(按市场市值计)就可以显著减少组合的碳排放强度,而不显著影响整体的回报率和风险水平。

论文来源

Building Benchmarks Portfolios with Decreasing Carbon Footprints [点击浏览原文]

<摘要>

本文构建了逐步减少碳足迹的投资组合,供被动投资者作为与《巴黎协定》一致的新基准使用,同时保持与传统基准相同的风险调整回报率。由于公司的碳强度分布极为偏斜,剔除一小部分高污染公司即可大幅降低公司股票组合的碳足迹。我们识别了全球范围内的最差污染企业,将其排除在组合之外,并重新分配资金,以保持行业和地区暴露与传统基准相似。这种方法限制了对新兴市场企业的撤资,同时确保区域暴露的稳定性。结果表明,通过连续剔除高达11%的公司,可以在10年内将组合的碳足迹减少64%

回测表现

年化收益率8.2%
波动率16.5%
Beta0.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)

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading