“通过气候情绪和碳价格交易美国股票,每月根据情绪-价格动态调整价值加权的EMC投资组合,通过买入或卖空,或投资于无风险资产。”

I. 策略概要

投资范围包括来自汤森路透Datastream的美国股票(纽约证券交易所和纳斯达克)。该策略结合了投资者气候情绪(使用StockTwits数据和sentimentr R包衡量)和碳价格(使用ICE-ECX碳排放配额(EUA)期货结算价格)。股票根据IPCC定义分为高排放或清洁股票。EMC投资组合做多高排放股票,做空清洁股票,两者均按价值加权。每月,如果气候情绪上升且碳价格下跌,则买入EMC投资组合;如果情绪下跌且碳价格上涨,则做空EMC投资组合。如果两者同向变动,则投资于无风险资产。投资组合按价值加权,每月重新平衡。

II. 策略合理性

先前的研究分析了与气候相关的Twitter帖子,但面临噪音和相关性问题。StockTwits,仅供投资者使用,提供了一个更清晰、更有针对性的数据集。该策略的成功取决于投资者情绪和时机。气候情绪受全球变暖和灾害等话题影响,进而影响股价:在高情绪时期,高排放股票被低估,清洁股票被高估,从而在EMC投资组合中产生错误定价。这种错误定价产生了正回报。股票被分为高排放和清洁两类,尽管“清洁”股票并非零排放,而是来自非碳密集型行业。该策略主要涉及根据气候情绪对高排放股票进行时机选择。

III. 来源论文

Investors’ Climate Sentiment and Financial Markets [点击查看论文]

<摘要>

我们建议通过对StockTwits上关于气候变化和全球变暖的帖子进行情感分析来衡量投资者气候情感。在金融市场中,当投资者气候情感更积极时,排放(碳密集型)公司的股票表现不及清洁(低排放)股票。我们记录了投资者对气候变化风险的过度反应以及长期反转。显著但非信息性的气候变化事件,例如气候变化报告的发布和异常天气事件,促进了投资者学习过程和错误定价的纠正。

IV. 回测表现

年化回报9.36%
波动率12.71%
β值0.056
夏普比率0.74
索提诺比率-0.139
最大回撤N/A
胜率43%

V. 完整的 Python 代码

from AlgorithmImports import *
from data_tools import QuantpediaClimateChange, QuantpediaFutures, ClimateChangeData, CarbonFutureData, CustomFeeModel, LastDateHandler
from typing import Dict, List
# endregion
class ClimateSentimentCarbonPricesAndEmissionMinusCleanPortfolio(QCAlgorithm):
    def Initialize(self) -> None:
        self.SetStartDate(2007, 1, 1)   # climate change data starts in 2004 and carbon future data starts in 2007
        self.SetCash(100_000)
        
        self.leverage: int = 5
        self.carbon_max_missing_days:int = 5
        self.climate_max_missing_days:int = 40
        self.min_prices: int = 15
        
        self.weights: Dict[Symbol, float] = {}
        self.high_emission_industries: List[MorningstarIndustryGroupCode] = [
            MorningstarIndustryGroupCode.Agriculture,
            MorningstarIndustryGroupCode.BuildingMaterials,
            MorningstarIndustryGroupCode.Chemicals,
            MorningstarIndustryGroupCode.Construction,
            MorningstarIndustryGroupCode.FarmAndHeavyConstructionMachinery,
            MorningstarIndustryGroupCode.ForestProducts,
            MorningstarIndustryGroupCode.HomebuildingAndConstruction,
            MorningstarIndustryGroupCode.MetalsAndMining,
            MorningstarIndustryGroupCode.OilAndGas,
            MorningstarIndustryGroupCode.Steel,
            MorningstarIndustryGroupCode.Transportation,
        ]
        self.market_symbol: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        self.climate_change: Symbol = self.AddData(QuantpediaClimateChange, 'CLIMATE_CHANGE', Resolution.Daily).Symbol
        self.climate_change_data: ClimateChangeData = ClimateChangeData()
        self.carbon_future: Symbol = self.AddData(QuantpediaFutures, 'ICE_EUA1', Resolution.Daily).Symbol
        self.carbon_future_data: CarbonFutureData = CarbonFutureData()
        self.fundamental_count: int = 500
        self.fundamental_sorting_key = lambda x: x.DollarVolume
        self.selection_flag: bool = False
        self.UniverseSettings.Leverage = self.leverage
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.settings.daily_precise_end_time = False
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.Schedule.On(self.DateRules.MonthStart(self.market_symbol), self.TimeRules.BeforeMarketClose(self.market_symbol, 0), self.Selection)
    def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())
    def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
        if not self.selection_flag:
            return Universe.Unchanged
        curr_date: datetime.date = self.Time.date()
        custom_data_last_update_date: Dict[Symbol, datetime.date] = LastDateHandler.get_last_update_date()
        if any((self.securities[symbol].get_last_data() and self.time.date() > custom_data_last_update_date[symbol]) for symbol in [self.climate_change, self.carbon_future]):
            self.Liquidate()
            return Universe.UNCHANGED
        # if not self.climate_change_data.data_still_coming(curr_date, self.climate_max_missing_days):
        #     self.climate_change_data.reset()
        # if not self.carbon_future_data.data_still_coming(curr_date, self.carbon_max_missing_days):
        #     self.carbon_future_data.reset()
        if not self.climate_change_data.is_ready() or not self.carbon_future_data.prices_ready(self.min_prices):
            self.carbon_future_data.reset()
            return Universe.Unchanged
        
        selected: List[Fundamental] = [
            x for x in fundamental 
            if x.HasFundamentalData 
            and x.MarketCap != 0
            and x.AssetClassification.MorningstarIndustryGroupCode 
        ]
        if len(selected) > self.fundamental_count:
            selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
        high_emission_stocks: List[Fundamental] = []
        clean_stocks: List[Fundamental] = []
        for stock in selected:
            industry_group_code: int = stock.AssetClassification.MorningstarIndustryGroupCode
            if industry_group_code in self.high_emission_industries:
                high_emission_stocks.append(stock)
            else:
                clean_stocks.append(stock)
        
        long_leg: List[Fundamental] = []
        short_leg: List[Fundamental] = []
        climate_monthly_change: float = self.climate_change_data.get_monthly_change()
        carbon_monthly_change: float = self.carbon_future_data.get_monthly_change()
        if climate_monthly_change > 0 and carbon_monthly_change < 0:
            long_leg = high_emission_stocks
            short_leg = clean_stocks
        elif climate_monthly_change < 0 and carbon_monthly_change > 0:
            long_leg = clean_stocks
            short_leg = high_emission_stocks
        if len(long_leg) != 0 and len(short_leg) != 0:
            for i, portfolio in enumerate([long_leg, short_leg]):
                mc_sum: float = sum(list(map(lambda stock: stock.MarketCap, portfolio)))
                for stock in portfolio:
                    self.weights[stock.Symbol] = ((-1)**i) * stock.MarketCap / mc_sum
        return list(self.weights.keys())
        
    def OnData(self, slice: Slice) -> None:
        curr_date:datetime.date = self.Time.date()
        if slice.contains_key(self.climate_change) and slice[self.climate_change]:
            search_value: float = slice[self.climate_change].Value
            self.climate_change_data.update(curr_date, search_value)
        if slice.contains_key(self.carbon_future) and slice[self.carbon_future]:
            price: float = slice[self.carbon_future].Value
            self.carbon_future_data.update(curr_date, price)
        # rebalance monthly
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        # trade execution
        portfolio: List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weights.items() if slice.contains_key(symbol) and slice[symbol]]
        self.SetHoldings(portfolio, True)
        self.weights.clear()
        
    def Selection(self) -> None:
        self.selection_flag = True

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读