
“该策略通过滚动一年期股息期货来捕捉股息风险溢价,通过做空SX5E来对冲股票风险敞口,使用基于每日回归的对冲比率进行有效的风险管理。”
资产类别: 期货 | 地区: 欧洲 | 周期: 每日 | 市场: 股票 | 关键词: 股息
I. 策略概要
该策略旨在通过模拟假设的1年期股息期货来保持对隐含股息的恒定敞口。这涉及每日在近月和远月股息期货之间滚动头寸,逐步将近月期货的权重降低至零,同时将远月期货的权重增加至100%。为了捕捉股息风险溢价,投资者做多“1年期恒定到期日”股息期货,并通过做空SX5E指数来对冲股票风险敞口。每日对冲比率通过过去两周股息期货回报与SX5E回报的线性回归确定,确保有效的风险管理。
II. 策略合理性
股息风险溢价的存在归因于几个因素。投资者高估了单只股票的股息削减风险,尽管年股息削减超过60%的情况很少见(自1991年以来为4%)。他们还低估了股息指数的多元化收益。流动性风险也是一个因素,因为股息期货的流动性较差。此外,系统性错误定价是由银行交易台持有大量SX5E股息多头头寸引起的。这些因素,包括对低概率、高影响力事件的风险厌恶和流动性挑战,在股息期货市场中创造了持续的溢价。
III. 来源论文
提取股息风险溢价 [点击查看论文]
- 布尔戈伊斯,汤姆(瑞士信贷)
<摘要>
在过去的几年里,股息不再被视为股权投资的副产品,它们构成独立资产类别的观点越来越普遍。虽然这主要与近期成熟、流动性强的股息期货市场的发展有关,但也有基本面和技术方面的原因将股息与股票或公司债券区分开来;特别是股息有时与股票的相关性较弱,且波动性较低。在本报告中,我们得出以下结论:股息遵循其自身的动态,股息风险溢价丰厚,构建有利可图的系统性股息策略是可能的。


IV. 回测表现
| 年化回报 | 12.9% |
| 波动率 | 11.6% |
| β值 | 0.227 |
| 夏普比率 | 1.11 |
| 索提诺比率 | N/A |
| 最大回撤 | -28.8% |
| 胜率 | 55% |
V. 完整的 Python 代码
from AlgorithmImports import *
from scipy import stats
import numpy as np
#endregion
class DividendRiskPremiumStrategy(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
self.dividend_futures = ['FEXD1', 'FEXD2', 'FEXD3']
self.fexd1 = None
for i, div_symbol in enumerate(self.dividend_futures):
data = self.AddData(QuantpediaFutures, div_symbol, Resolution.Daily)
data.SetLeverage(10)
data.SetFeeModel(CustomFeeModel())
# store nearby contract
if i == 0:
self.fexd1 = data.Symbol
# STOXX 50 future
data = self.AddData(QuantpediaFutures, "EUREX_FSTX1", Resolution.Daily)
data.SetLeverage(10)
data.SetFeeModel(CustomFeeModel())
self.SX5E = data.Symbol
# daily price
self.data = {}
self.period = 10
self.max_missing_days = 5
for symbol in [self.fexd1, self.SX5E]:
self.data[symbol] = RollingWindow[float](self.period)
self.selection_flag = False
self.recent_month:int = -1
def OnData(self, data):
# store daily prices
if self.fexd1 in data and data[self.fexd1] and self.SX5E in data and data[self.SX5E]:
self.data[self.fexd1].Add(data[self.fexd1].Value)
self.data[self.SX5E].Add(data[self.SX5E].Value)
else:
if (self.Securities[self.fexd1].GetLastData() and (self.Time.date() - self.Securities[self.fexd1].GetLastData().Time.date()).days > self.max_missing_days) or \
(self.Securities[self.SX5E].GetLastData() and (self.Time.date() - self.Securities[self.SX5E].GetLastData().Time.date()).days > self.max_missing_days):
self.data[self.fexd1].Reset()
self.data[self.SX5E].Reset()
self.Liquidate()
if self.recent_month == self.Time.month:
return
self.recent_month = self.Time.month
if self.Time.month in [7, 1]:
self.Liquidate()
self.weights = [1, 0] # divident future #2 and #3 weights
self.selection_flag = True
if self.Portfolio.Invested or self.selection_flag:
if self.data[self.fexd1].IsReady and self.data[self.SX5E].IsReady:
dividend_future_prices = np.array([x for x in self.data[self.fexd1]])
market_prices = np.array([x for x in self.data[self.SX5E]])
dividend_future_returns = dividend_future_prices[:-1] / dividend_future_prices[1:] - 1
market_returns = market_prices[:-1] / market_prices[1:] - 1
if not all(x==0 for x in dividend_future_returns):
slope, intercept, r_value, p_value, std_err = stats.linregress(market_returns, dividend_future_returns)
# dividend futures exposure
self.SetHoldings(self.dividend_futures[1], self.weights[0])
self.SetHoldings(self.dividend_futures[2], self.weights[1])
# adjust weights every invested day
if self.weights[0] >= 0.008:
self.weights[0] -= 0.008
if self.weights[0] <= 0.992:
self.weights[1] += 0.008
# hedge
self.SetHoldings(self.SX5E, -abs(slope))
self.selection_flag = False
else:
self.Liquidate()
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaFutures(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config, line, date, isLiveMode):
data = QuantpediaFutures()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(';')
data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1)
data['back_adjusted'] = float(split[1])
data['spliced'] = float(split[2])
data.Value = float(split[1])
return data
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))