
“该策略使用GDP增长缺口因子在全球五个国家配置10年期债券期货,通过等权重调整并每季度重新平衡,以实现系统化投资。”
资产类别: 差价合约、期货 | 地区: 全球 | 周期: 每季度 | 市场: 债券 | 关键词: 缺口因子
I. 策略概要
该策略涉及澳大利亚、加拿大、德国、英国和美国的10年期政府债券期货,使用GDP增长缺口因子(3年缺口、1年缺口或1年变化)。配置策略可以遵循底部、中位数或顶部版本,或专注于单一因子。投资组合通过按比例买卖债券期货来构建,基于横截面分数,该分数将指标与横截面平均值(根据离散度调整)进行比较。通过对各国进行等权重配置来消除方向性偏差,并每日调整。由于数据可用性,投资组合每季度重新平衡,利用GDP增长趋势进行系统性债券投资决策。
II. 策略合理性
经济增长与债券之间存在直观的联系。研究表明,在假设消费等于产出且忽略不确定性的情况下,实际利率可能会围绕经济增长波动。短期利率受中央银行影响以引导产出增长,显示出与增长的某种联系。对于长期利率,债务/GDP是关键驱动因素,而增长缺乏持续的影响。尽管没有基本面解释为何增长缺口能够预测债券期货,但统计和机器学习分析将其识别为一个可靠的因子。增长缺口在投资组合构建中的使用源于这些分析,证明了其在改进投资策略方面的有效性。
III. 来源论文
Beyond Carry and Momentum in Government Bonds [点击查看论文]
- 热罗姆·加瓦(Jerome Gava)、威廉·勒费布尔(William Lefebvre)和朱利安·特克(Julien Turc)
<摘要>
本文回顾了近期关于政府债券因子投资的文献,特别是关于价值和防御性投资的定义。作者使用机器学习技术,识别了政府债券期货的关键驱动因素和最真正相关的因子组。除了利差和动量之外,他们提出了一种防御性投资方法,该方法考虑了政府债券的避险性质。这两种主要风格可以辅以价值和反转因子,以实现独立于利率广泛变动的回报。
IV. 回测表现
| 年化回报 | 1% |
| 波动率 | 9.99% |
| β值 | -0.004 |
| 夏普比率 | 0.1 |
| 索提诺比率 | -1.53 |
| 最大回撤 | -39% |
| 胜率 | 41% |
V. 完整的 Python 代码
import numpy as np
from AlgorithmImports import *
import data_tools
from typing import Dict, List
class GrowthGapFactorinFixedIncome(QCAlgorithm):
def Initialize(self):
self.SetStartDate(1990, 1, 1)
self.SetCash(100000)
# Bond symbol and GDP symbol. (GDP at current prices)
self.symbols = {
"ASX_XT1" : "AUS_GDP", # 10 Year Commonwealth Treasury Bond Futures, Continuous Contract #1 (Australia)
"MX_CGB1" : "CAN_GDP", # Ten-Year Government of Canada Bond Futures, Continuous Contract #1 (Canada)
"EUREX_FGBL1" : "DEU_GDP", # Euro-Bund (10Y) Futures, Continuous Contract #1 (Germany)
"LIFFE_R1" : "GBR_GDP", # Long Gilt Futures, Continuous Contract #1 (U.K.)
"CME_TY1" : "USA_GDP" # 10 Yr Note Futures, Continuous Contract #1 (USA)
}
# Yearly GDP data used for SMA.
self.data:Dict[str, float] = {}
self.sma_period:int = 5
self.leverage:int = 3
for bond_future, gdp_symbol in self.symbols.items():
# Futures data.
data = self.AddData(data_tools.QuantpediaFutures, bond_future, Resolution.Daily)
data.SetFeeModel(data_tools.CustomFeeModel())
data.SetLeverage(self.leverage)
self.data[gdp_symbol] = RollingWindow[float](self.sma_period)
# Bond yield data.
self.AddData(data_tools.GDPData, gdp_symbol, Resolution.Daily)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
def OnData(self, data: Slice) -> None:
trade_flag:bool = False
gdp_last_update_date:Dict[str, datetime.date] = data_tools.GDPData.get_last_update_date()
future_last_update_date:Dict[str, datetime.date] = data_tools.QuantpediaFutures.get_last_update_date()
symbols_to_delete:List[str] = []
# store yearly gdp data
for bond_future, gdp_symbol in self.symbols.items():
# data is still coming
if self.Securities[bond_future].GetLastData() and self.Time.date() > future_last_update_date[bond_future] \
or self.Securities[gdp_symbol].GetLastData() and self.Time.date() > gdp_last_update_date[gdp_symbol]:
symbols_to_delete.append(bond_future)
continue
if gdp_symbol in data and data[gdp_symbol]:
gdp:float = data[gdp_symbol].Value
self.data[gdp_symbol].Add(gdp)
trade_flag = True
if len(symbols_to_delete) != 0:
for symbol in symbols_to_delete:
self.symbols.pop(symbol)
# rebalance once the new data arrived
if not trade_flag:
return
# SMA gap
sma_gap:Dict[str, float] = { x[0] : ((self.Securities[x[1]].Price - np.average([gdp for gdp in self.data[x[1]]])) / np.average([gdp for gdp in self.data[x[1]]])) for x in self.symbols.items()
if self.Securities.ContainsKey(x[1]) and x[1] in self.data and self.data[x[1]].IsReady and self.Securities[x[0]].GetLastData() and (self.Time.date() - self.Securities[x[0]].GetLastData().Time.date()).days < 5}
weight:Dict[str, float] = {}
if len(sma_gap) != 0:
avg_gap:float = np.average([x[1] for x in sma_gap.items()])
avg_gap_diff:Dict[str, float] = {x[0] : x[1] - avg_gap for x in sma_gap.items()}
total_avg_gap_diff:float = sum([abs(x[1]) for x in avg_gap_diff.items()])
weight = {x[0] : x[1] / total_avg_gap_diff for x in avg_gap_diff.items()}
# trade execution
invested:List[str] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in weight:
self.Liquidate(symbol)
for symbol, w in weight.items():
if symbol in data and data[symbol]:
self.SetHoldings(symbol, w)