“该策略使用GDP增长缺口因子在全球五个国家配置10年期债券期货,通过等权重调整并每季度重新平衡,以实现系统化投资。”

I. 策略概要

该策略涉及澳大利亚、加拿大、德国、英国和美国的10年期政府债券期货,使用GDP增长缺口因子(3年缺口、1年缺口或1年变化)。配置策略可以遵循底部、中位数或顶部版本,或专注于单一因子。投资组合通过按比例买卖债券期货来构建,基于横截面分数,该分数将指标与横截面平均值(根据离散度调整)进行比较。通过对各国进行等权重配置来消除方向性偏差,并每日调整。由于数据可用性,投资组合每季度重新平衡,利用GDP增长趋势进行系统性债券投资决策。

II. 策略合理性

经济增长与债券之间存在直观的联系。研究表明,在假设消费等于产出且忽略不确定性的情况下,实际利率可能会围绕经济增长波动。短期利率受中央银行影响以引导产出增长,显示出与增长的某种联系。对于长期利率,债务/GDP是关键驱动因素,而增长缺乏持续的影响。尽管没有基本面解释为何增长缺口能够预测债券期货,但统计和机器学习分析将其识别为一个可靠的因子。增长缺口在投资组合构建中的使用源于这些分析,证明了其在改进投资策略方面的有效性。

III. 来源论文

Beyond Carry and Momentum in Government Bonds [点击查看论文]

<摘要>

本文回顾了近期关于政府债券因子投资的文献,特别是关于价值和防御性投资的定义。作者使用机器学习技术,识别了政府债券期货的关键驱动因素和最真正相关的因子组。除了利差和动量之外,他们提出了一种防御性投资方法,该方法考虑了政府债券的避险性质。这两种主要风格可以辅以价值和反转因子,以实现独立于利率广泛变动的回报。

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)

发表评论

了解 Quant Buffet 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读