
“该策略涉及根据公司年度排放量变化(按销售额衡量)对公司进行排序,做多排名前五分之一(排放量减少最大)的公司,做空排名后五分之一的公司。每年重新平衡。”
资产类别: 股票 | 地区: 全球 | 周期: 每年 | 市场: 股票 | 关键词: 有毒物质
I. 策略概要
投资范围包括来自美国环保署有毒物质排放清单中具有有毒物质排放数据的股票。公司根据年度排放量变化(按销售额衡量)进行排序。该策略涉及做多排名前五分之一(排放量减少最大)的公司,做空排名后五分之一的公司。投资组合采用价值加权,每年重新平衡。
II. 策略合理性
绿色企业中的异常回报可归因于全球对环境问题日益增长的意识以及对可持续产品的日益偏好。随着消费者和投资者转向更环保的做法,优先考虑可持续发展的公司,例如那些减少排放的公司,往往会吸引长期资本,特别是来自机构投资者的资本。研究发现,机构投资者更有可能减持产生更多有毒排放物且经营业绩不佳的公司,同时投资于盈利预测更好的绿色公司。这表明投资者最初可能忽视了绿色企业的潜力,当这些企业表现超出预期时,导致异常回报。最终,这一趋势凸显了可持续发展在推动长期财务成功方面日益增长的重要性。
III. 来源论文
The Value of Going Green [点击查看论文]
- 金泰贤(Taehyun Kim),中央大学(Chung-Ang University)
<摘要>
我们调查了企业环境责任(CER)行动如何影响公司价值。我们采用事件研究方法,利用两项以5比4微弱优势裁定的最高法院判决,表明当企业预计会增加其CER活动时,它们会获得价值。受法院判决影响程度更大的企业,市场反应也越大。这些回报模式在社会信任度高的地区的企业中更为显著。减少有毒化学品排放的企业显示出正的盈利意外、更高的收入和盈利能力,以及来自具有更长投资期限的机构投资者的更大资本流入。总而言之,我们提供了与市场认为CER会带来更高公司价值相符的经验证据。

IV. 回测表现
| 年化回报 | 4.51% |
| 波动率 | 10.5% |
| β值 | 0.19 |
| 夏普比率 | 0.43 |
| 索提诺比率 | -0.06 |
| 最大回撤 | N/A |
| 胜率 | 47% |
V. 完整的 Python 代码
from AlgorithmImports import *
import data_tools
from numpy import isnan
from typing import List, Dict
# endregion
class ToxicalReleasesandStocksPerformance(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.leverage: int = 10
self.quantile: int = 5
self.toxic_release_data: Dict[Dict[datetime.date, str]] = {}
self.report_dates: Dict[datetime.date, datetime.date] = {}
self.last_data: Dict[str, float] = {}
self.last_year: int = -1
self.weight: Dict[Symbol, float] = {}
# Data source: https://peri.umass.edu/top-100-indexes-archives
toxic_release_data: str = self.Download('data.quantpedia.com/backtesting_data/economic/toxic_releases.json')
toxic_release_data_json: Dict[str] = json.loads(toxic_release_data)
for obj in toxic_release_data_json:
report_date: int = datetime.strptime(obj['report_date'], "%Y").year
data_date: int = datetime.strptime(obj['data_date'], "%Y").year
if report_date not in self.toxic_release_data:
self.toxic_release_data[report_date] = {}
self.report_dates[report_date] = data_date
for stock_data in obj['stocks']:
ticker: str = stock_data['ticker']
try:
toxic_release: float = float(stock_data['toxic air release[mil./lb]'])
except:
continue
self.toxic_release_data[report_date][ticker] = toxic_release
self.current_year: int = -1
self.selection_flag: bool = False
self.settings.daily_precise_end_time = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(self.leverage)
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
# rebalance yearly
if self.Time.year == self.current_year:
return Universe.Unchanged
self.current_year = self.Time.year
if self.Time.year not in self.toxic_release_data:
return Universe.Unchanged
current_report_year = self.Time.year
current_data_year = self.report_dates[current_report_year]
selected: List[Fundamental] = [x for x in fundamental if x.Symbol.Value in list(self.toxic_release_data[current_report_year]) and x.MarketCap != 0 and \
not isnan(x.FinancialStatements.IncomeStatement.TotalRevenue.TwelveMonths) and x.FinancialStatements.IncomeStatement.TotalRevenue.TwelveMonths != 0]
emissions_by_dollar: Dict[Fundamental, float] = {}
if len(self.last_data) != 0:
# check if data is present
for stock in selected:
if stock.Symbol.Value in self.last_data:
toxic_release_change: float = self.toxic_release_data[current_report_year][stock.Symbol.Value] - self.last_data[stock.Symbol.Value]
emissions_dolar: float = toxic_release_change / stock.FinancialStatements.IncomeStatement.TotalRevenue.TwelveMonths
emissions_by_dollar[stock] = emissions_dolar
# save current data for next selection
self.last_data.clear()
self.last_data = {stock.Symbol.Value: self.toxic_release_data[current_report_year][stock.Symbol.Value] for stock in selected}
self.last_year = self.report_dates[self.Time.year]
if len(emissions_by_dollar) >= self.quantile:
self.selection_flag = True
# sort stocks and divide into quantiles
sorted_stocks: List[Fundamental] = sorted(emissions_by_dollar, key=emissions_by_dollar.get, reverse=True)
quantile: int = int(len(sorted_stocks) / self.quantile)
long: List[Fundamental] = sorted_stocks[:quantile]
short: List[Fundamental] = sorted_stocks[-quantile:]
# value weighting
for i, portfolio in enumerate([long, short]):
mc_sum: float = sum(list(map(lambda stock: stock.MarketCap, portfolio)))
for stock in portfolio:
self.weight[stock.Symbol] = ((-1)**i) * stock.MarketCap / mc_sum
return list(self.weight.keys())
def OnData(self, data: Slice) -> None:
if not self.selection_flag:
return
self.selection_flag = False
# trade execution
invested: List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in self.weight:
self.Liquidate(symbol)
for symbol, weight in self.weight.items():
if symbol in data and data[symbol]:
self.SetHoldings(symbol, weight)
self.weight.clear()
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
fee: float = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))