
“该策略涉及根据过去12个月商品回报的z分数在10年期债券中开立多头或空头头寸,头寸大小由z分数的绝对值决定,并每月重新平衡。”
资产类别: 债券、期货 | 地区: 全球 | 周期: 每月 | 市场: 债券 | 关键词: 债券
I. 策略概要
该投资范围包括来自美国、英国、德国、日本、加拿大和澳大利亚的10年期债券。头寸根据过去12个月商品回报的z分数开立,计算方法是:近期12个月回报减去10年平均回报,然后除以标准差。为了限制极端情况,z分数上限设置为1和-1。如果z分数为正,则采取债券空头头寸;如果为负,则开立多头头寸。头寸大小基于z分数的绝对值。投资组合等权重,每月重新平衡。变体包括仅使用z分数符号或不设上限。
II. 策略合理性
该策略基于商品价格预示通胀压力的理念。商品价格下跌表明通货紧缩,导致债券回报率上升,而价格上涨则预示债券回报率下降。路透CRB总回报指数被用作通胀压力的代理。该策略的可预测性在各种经济条件下都表现稳健,包括衰退和扩张时期、高通胀和低通胀时期,以及股票牛市和熊市。这种可预测性归因于债券市场择时,而非结构性债券风险,并且在市场波动较大时表现出更强的业绩。
III. 来源论文
Predicting Bond Returns: 70 years of International Evidence [点击查看论文]
- Guido Baltussen, Martin Martens, Olaf Penninga,鹿特丹伊拉斯姆斯大学(EUR);北方信托公司 – 北方信托资产管理,鹿特丹伊拉斯姆斯大学,鹿博资产管理
<摘要>
我们通过对主要债券市场70年国际数据的深入研究,考察了政府债券回报的可预测性。利用基于经济交易的测试框架,我们发现了债券回报可预测性的强有力经济和统计证据,自1950年以来夏普比率为0.87。这一发现对市场和时间段都具有稳健性,包括30年的国际债券市场样本外数据和另外九个国家的数据。此外,结果在各种经济环境中保持一致,包括利率长期上升或下降的时期,并且在扣除交易成本后仍可利用。可预测性与通胀和经济增长的可预测性相关。总的来说,政府债券溢价显示出可预测的动态,国际债券市场回报的择时为投资者提供了可利用的机会。


IV. 回测表现
| 年化回报 | 4.4% |
| 波动率 | 10% |
| β值 | 0.091 |
| 夏普比率 | 0.44 |
| 索提诺比率 | -0.149 |
| 最大回撤 | N/A |
| 胜率 | 53% |
V. 完整的 Python 代码
from AlgorithmImports import *
import numpy as np
#endregion
class PredictingBondReturnswithCommodityIndex(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.period = (10 * 12) * 21 + 21
self.SetWarmUp(self.period, Resolution.Daily)
data = self.AddData(QuantpediaIndices, 'SPGSCITR', Resolution.Daily)
data.SetFeeModel(CustomFeeModel())
self.symbol = data.Symbol
# Daily price data.
self.data = RollingWindow[float](self.period)
self.max_missing_days:int = 5
self.recent_month:int = -1
def OnData(self, data):
# Store daily data.
if self.symbol in data and data[self.symbol]:
price = data[self.symbol].Value
self.data.Add(price)
if self.recent_month == self.Time.month:
return
self.recent_month = self.Time.month
self.Liquidate()
# Z score calc.
z_score = 0
if self.data.IsReady:
if self.Securities[self.symbol].GetLastData() and (self.Time.date() - self.Securities[self.symbol].GetLastData().Time.date()).days <= self.max_missing_days:
closes:List[float] = list(self.data)[::-1]
separete_yearly_returns = [Return(closes[x:x+12*21]) for x in range(0, len(closes),21)]
# Return history is ready.
if len(separete_yearly_returns) == self.period / 21:
return_mean = np.mean(separete_yearly_returns)
return_std = np.std(separete_yearly_returns)
z_score = (separete_yearly_returns[-1] - return_mean) / return_std
if z_score > 1: z_score = 1
elif z_score < -1: z_score = -1
z_score = -1 * z_score
self.SetHoldings(self.symbol, z_score)
# Quantpedia bond yield data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaIndices(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource("data.quantpedia.com/backtesting_data/index/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config, line, date, isLiveMode):
data = QuantpediaIndices()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(',')
data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
data['close'] = float(split[1])
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"))
def Return(values):
return (values[-1] - values[0]) / values[0]