“该策略交易来自六个国家的10年期债券期货,使用基于股票波动率的因子来分配头寸,通过等权重每日调整和重新平衡来消除方向性偏差,以提高精确度。”

I. 策略概要

该策略侧重于来自六个国家(澳大利亚、加拿大、德国、日本、英国和美国)的10年期政府债券期货。它构建了一个基于股票波动率(三年、一年或一个月)的因子,其中高波动率预示着正向债券配置,反之亦然。配置可以遵循底部、中位数或顶部策略,或者跨版本采用组合方法。因子投资组合通过按比例买卖债券期货来构建,使用通过将指标与调整后的横截面平均值进行比较得出的横截面分数。通过对所有国家进行等权重配置来消除方向性偏差,并每日调整。该策略每日重新平衡以确保精确性。

II. 策略合理性

防御性债券投资符合债券作为避险资产的声誉,研究表明股票市场波动性指导防御性债券策略。当信心下降时,投资者从股票转向债券,这种现象与股票市场冲击期间的“避险”效应相关。相反,债券市场冲击对债券和股票都产生负面影响。股票市场波动性是债券投资的有用指标,因为它反映了投资者情绪和风险规避行为,使其成为把握防御性债券策略时机的重要工具。这一洞察解释了为什么股票市场波动性可以有效地用于债券市场决策。

III. 来源论文

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

<摘要>

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

IV. 回测表现

年化回报2.8%
波动率8.2%
β值0.013
夏普比率0.34
索提诺比率-1.898
最大回撤-15%
胜率49%

V. 完整的 Python 代码

import numpy as np
from AlgorithmImports import *
class FlighttoQualityFactor(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        
        self.symbols = {
                        "EWA" : "ASX_XT1",        # 10 Year Commonwealth Treasury Bond Futures, Continuous Contract #1 (Australia)
                        "EWC" : "MX_CGB1",        # Ten-Year Government of Canada Bond Futures, Continuous Contract #1 (Canada)
                        "EWG" : "EUREX_FGBL1",    # Euro-Bund (10Y) Futures, Continuous Contract #1 (Germany)
                        "EWJ" : "SGX_JB1",        # SGX 10-Year Mini Japanese Government Bond Futures, Continuous Contract #1 (Japan)
                        "EWU" : "LIFFE_R1",       # Long Gilt Futures, Continuous Contract #1 (U.K.)
                        "SPY" : "CME_TY1"         # 10 Yr Note Futures, Continuous Contract #1 (USA)
                        }
        self.data = {}
        self.period = 12 * 21
        leverage: int = 2
        self.SetWarmUp(self.period)
        for symbol in self.symbols:
            bond = self.symbols[symbol]
            
            self.AddEquity(symbol, Resolution.Daily)
            self.data[symbol] = RollingWindow[float](self.period)
            data = self.AddData(QuantpediaFutures, bond, Resolution.Daily)
            data.set_leverage(leverage)
            data.SetFeeModel(CustomFeeModel())
            
    def OnData(self, data):
        for symbol, bond in self.symbols.items():
            if self.securities[bond].get_last_data() and self.time.date() > QuantpediaFutures.get_last_update_date()[bond]:
                self.liquidate(bond)
                self.data[symbol].reset()
                continue
            if symbol in data and data[symbol]:
                price = data[symbol].Value
                self.data[symbol].Add(price)
        if self.IsWarmingUp: return
    
        volatility = {}
        for symbol in self.symbols:
            if self.data[symbol].IsReady:
                prices = np.array([x for x in self.data[symbol]])
                returns = prices[:-1] / prices[1:] - 1
                volatility[symbol] = np.std(returns)
        
        if len(volatility) <= 1: return
        
        avg_volatility = np.mean([x[1] for x in volatility.items()])
        
        diff = {symbol : (volatility[symbol] - avg_volatility) for symbol in volatility}
        total_diff = sum([abs(x[1]) for x in diff.items()])
        
        for symbol in diff:
            bond = self.symbols[symbol]
            if data.contains_key(bond) and data[bond]:
                self.SetHoldings(bond, diff[symbol] / total_diff)
# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaFutures(PythonData):
    _last_update_date:Dict[Symbol, datetime.date] = {}
    @staticmethod
    def get_last_update_date() -> Dict[Symbol, datetime.date]:
       return QuantpediaFutures._last_update_date
    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])
        if config.Symbol.Value not in QuantpediaFutures._last_update_date:
            QuantpediaFutures._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()
        if data.Time.date() > QuantpediaFutures._last_update_date[config.Symbol.Value]:
            QuantpediaFutures._last_update_date[config.Symbol.Value] = data.Time.date()
        return data

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读