“该策略涉及10种兑美元的货币,按Refinitiv的ESG评级进行排名。每个国家的ESG分数是其公司评级的平均值,欧元区使用其公司平均评级。货币每月根据之前的ESG评级分为五个分位数。对ESG分数最低的货币做多,对ESG分数最高的货币做空。投资组合等权重,每月重新平衡。”

I. 策略概要

该投资框架关注10种货币:澳大利亚、加拿大、丹麦、欧元区(欧元)、日本、新西兰、挪威、瑞典、瑞士和英国,所有这些货币都以美元为基准。使用的主要指标是ESG(环境、社会和治理)评级,来源于Refinitiv。为了计算国家层面的ESG评级,方法是平均每个国家内所有被评级公司的ESG分数。对于欧元区,平均值来源于该地区的公司。

投资组合每月根据前一个月的ESG评级将货币分为五个分位数。ESG分数最低的货币组成第一个投资组合,而分数最高的货币组成最后一个投资组合。交易策略涉及做多第一个投资组合(ESG分数最低的货币)并做空最后一个投资组合(ESG分数最高的货币)。所有投资组合均等权重,并在每个月末进行再平衡。

II. 策略合理性

该策略借鉴了行为金融学的原理。来自高ESG评分国家的货币通常被视为稳定且低风险,在政治、环境或社会动荡时期,投资者寻求避险资产时,这类货币更具吸引力。然而,这种被认为安全的特性通常伴随着较低的回报。

相比之下,来自低ESG评分国家的货币被认为风险更高,通常反映出更大的不稳定性。为了吸引投资,这些货币通常会以更高的回报率作为风险溢价。该策略正是利用了这一动态:高ESG货币在危机期间提供保护,但回报较低;而低ESG货币虽然风险较高,但作为补偿,提供了更高的潜在回报。

III. 来源论文

Pricing Ethics in the Foreign Exchange Market: Environmental, Social and Governance Ratings and Currency Premia [点击查看论文]

<摘要>

我们研究了Refinitiv环境、社会和治理(ESG)评分在外汇市场回报中的横截面预测能力,使用国家层面汇总的ESG评分,发现ESG是货币回报的强负向预测因子。直观地讲,投资者需要为资助低ESG国家而获得溢价,而高ESG国家则提供较低的回报并在世界处于不良状态时提供对冲。我们表明ESG在货币回报的横截面中被定价。我们还考虑了ESG的不同组成部分,并表明其可预测性是由ESG评级的环境支柱驱动的。ESG货币策略的盈利能力并非由套利交易驱动,并且对交易成本具有稳健性。

IV. 回测表现

年化回报3.62%
波动率7.03%
β值0.038
夏普比率0.52
索提诺比率N/A
最大回撤N/A
胜率60%

V. 完整的 Python 代码

from AlgorithmImports import *
import data_tools
#endregion
class ESGInCurrencies(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2016, 1, 1) # first esg data are from 2016
        self.SetCash(100000)
        
        # switching ratings from letters to number for easier sorting
        self.rating_switcher = {
            'AAA': 9,
            'AA': 8,
            'A': 7,
            'BBB': 6,
            'BB': 5,
            'B': 4,
            'CCC': 3,
            'CC': 2,
            'C': 1,
        }
        
        self.indexes_currencies = {
            'ASX': 'AUDUSD', # Australia
            # 'DJ30':  # USA
            'EUROSTOXX': 'EURUSD', # Europe
            'FTSE100': 'GBPUSD', # Britain
            'NIKKEI': 'USDJPY', # Japan
            'NZX50': 'NZDUSD', # New Zeland
            'SMI': 'USDCHF', # Switzerland
            'TSX': 'USDCAD' # Canada
        }
        
        self.securities_count = 2 # long n securities and short n securities
        self.max_missing_days = 31
        
        self.data = {} # storing objects of SymbolData class keyed by tickers
        
        # subscribe to esg indexes data
        for index_ticker, currency_ticker in self.indexes_currencies.items():
            index_esg = self.AddData(data_tools.IndexESG, index_ticker, Resolution.Daily)
            currency = self.AddForex(currency_ticker, Resolution.Daily, Market.Oanda)
            # currency = self.AddData(data_tools.QuantpediaFutures, currency_ticker, Resolution.Daily)
            currency.SetLeverage(5)
            
            # create SymbolData object for current index
            self.data[index_ticker] = data_tools.SymbolData(index_esg.Symbol, currency.Symbol)
            
        self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        
        self.months_counter = 0    
        self.selection_flag = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.Schedule.On(self.DateRules.MonthStart(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)
    def OnData(self, data):
        # update esg data on daily basis
        for index_ticker, symbol_obj in self.data.items():
            index_esg_symbol = symbol_obj.index_esg_symbol
            
            if index_esg_symbol in data and data[index_esg_symbol]:
                # get ratings for current date
                ratings = [x for x in data[index_esg_symbol].Ratings]
                # get and convert valid ratings
                valid_ratings:list = self.GetValidRatings(ratings)
                # add valid ratings to all index ratings for current year
                symbol_obj.esg_ratings = symbol_obj.esg_ratings + valid_ratings
        
        # rebalance yearly
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        # get mean of index esg rating keyed by currency symbol
        mean_esg_ratings:dict = {}
        
        for index_ticker, symbol_obj in self.data.items():
            if self.Securities[index_ticker].GetLastData() and (self.Time.date() - self.Securities[index_ticker].GetLastData().Time.date()).days > self.max_missing_days:
                continue
            # check if esg data are ready
            if len(symbol_obj.esg_ratings) <= 0:
                continue
            
            currency_symbol = symbol_obj.currency_symbol
            
            # calculate index mean esg rating
            mean_esg_rating_value = symbol_obj.mean_esg_rating_value()
            
            # store index mean esg rating keyed by currency symbol
            mean_esg_ratings[currency_symbol] = mean_esg_rating_value
        
        # clear indexes ratings after mean calculation
        for index_ticker, symbol_obj in self.data.items():
            symbol_obj.esg_ratings.clear()
            
        # make sure, there are enough currencies for trade
        if len(mean_esg_ratings) < (self.securities_count * 2 + 1):
            self.Liquidate()
            return
        
        # sort currencies symbols based on mean esg ratings
        sorted_by_mean_esg_rating = [x[0] for x in sorted(mean_esg_ratings.items(), key=lambda item: item[1])]
        
        # long currencies with lowest mean esg rating
        long = sorted_by_mean_esg_rating[:self.securities_count]
        # short currencies with highest mean esg rating
        short = sorted_by_mean_esg_rating[-self.securities_count:]
        
        # trade execution
        self.Liquidate()
        
        for symbol in long + short:
            ticker = symbol.Value
            # long currency against USD based on plus or minus sign
            weight:float = self.GetWeight(ticker)
            self.SetHoldings(symbol, weight)
        
    def GetValidRatings(self, ratings):
        ''' retrieve, transform and return all valid ratings '''
        
        valid_ratings = []
        
        for rating in ratings:
            if rating != '-1':
                # switch rating to valid rating value for next calculations
                valid_rating_value = self.rating_switcher[rating]
                valid_ratings.append(valid_rating_value)
                
        return valid_ratings
        
    def GetWeight(self, ticker):
        # calculate equally weight
        weight = 1 / self.securities_count
         
        # change math sign, if needed  
        if ticker[:3] == 'USD':
            weight = weight * -1
            
        return weight
        
    def Selection(self):
        # yearly rebalance
        if self.months_counter % 12 == 0:
            self.selection_flag = True
        self.months_counter += 1

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读