
“该策略涉及10种兑美元的货币,按Refinitiv的ESG评级进行排名。每个国家的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 [点击查看论文]
- Ilias Filippou 和 Mark P. Taylor。佛罗里达州立大学。圣路易斯华盛顿大学 – 约翰·M·奥林商学院;经济政策研究中心(CEPR);布鲁金斯学会
<摘要>
我们研究了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