研究表明,大宗商品投资与风险偏好密切相关,从而促进了套利交易的表现。货币波动率和TED利差能够有效预测套利交易的表现,反映市场压力和全球流动性变化的影响。

I. 策略概述

研究表明,企业信用违约掉期(CDS)利差的期限结构(五年期CDS利差减去一年期利差)可以用于预测股票收益。具体而言,CDS期限结构斜率较低(较平)的股票在未来六个月内平均跑赢CDS期限结构斜率较高(较陡)的股票,每月超额收益超过1%。此策略通过识别CDS期限结构斜率较低的股票构建多头头寸,同时对CDS斜率较高的股票构建空头头寸,投资组合按等权重配置,并按月进行再平衡。

II. 策略合理性

学术研究指出,陡峭的CDS期限结构可能表明投资者预期企业信用质量恶化以及未来CDS利差扩大。这一预期通常因信息扩散的滞后性而逐步反映在股价中。CDS期限结构斜率较低的股票通常与更高的市场信心和更低的信用风险相关,这使得这些股票在未来表现优于期限结构斜率较高的股票。通过利用这一关联,该策略捕捉市场中由信用预期变化驱动的定价异常。

III. 论文来源

Term Structure of Credit Default Swap Spreads and Cross-Section of Stock Returns [点击浏览原文]

<摘要>

企业信用违约掉期(CDS)利差期限结构的斜率(五年期减去一年期的利差)能够显著预测未来的股票收益。CDS斜率较低的股票在未来六个月内平均跑赢CDS斜率较高的股票,每月超额收益超过1%。这一结果无法通过标准风险因子、股票特征、违约风险测量或CDS利差变化解释。研究进一步发现,CDS期限结构斜率与市场定价之间存在一种独立于传统资产定价模型的强相关性,这表明CDS期限结构在预测股票收益方面具有重要作用。

IV. 回测表现

年化收益率27.57%
波动率23.84%
Beta0.166
夏普比率0.99
索提诺比率-0.096
最大回撤N/A
胜率50%

V. 完整python代码

from AlgorithmImports import *
import data_tools
from typing import List, Dict
#endregion
class CombinedStockandCDSMomentum(QCAlgorithm):
    def Initialize(self) -> None:
        self.SetStartDate(2003, 1, 1)
        self.SetCash(100_000)
        self.UniverseSettings.Leverage = 5
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.0
        self.settings.daily_precise_end_time = False
        
        self.quantile: int = 10
        self.selection_flag: bool = False
        self.tickers: List[str] = []
        self.long_symbols: List[Symbol] = []
        self.short_symbols: List[Symbol] = []
        self.cds_1y: Symbol = self.AddData(data_tools.EquityCDS1Y, 'CDS1Y', Resolution.Daily).Symbol
        self.cds_5y: Symbol = self.AddData(data_tools.EquityCDS5Y, 'CDS5Y', Resolution.Daily).Symbol
        market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        self.Schedule.On(self.DateRules.MonthStart(market), 
                        self.TimeRules.AfterMarketOpen(market), 
                        self.Selection)
    def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
        for security in changes.AddedSecurities:
            security.SetFeeModel(data_tools.CustomFeeModel())
    def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
        slope: Dict[Symbol, int] = {}
        
        # calculate slope of the CDS term structure
        if self.Securities.ContainsKey(self.cds_1y) and self.Securities.ContainsKey(self.cds_5y):
            cds_1y_data = self.Securities[self.cds_1y].GetLastData()
            cds_5y_data = self.Securities[self.cds_5y].GetLastData()
            
            if cds_1y_data and cds_5y_data:
                # data has not been initialized yet
                if len(self.tickers) == 0:
                    self.tickers = list([x.upper() for x in cds_1y_data.GetStorageDictionary().Keys])
                
                if self.selection_flag:
                    for f in fundamental:
                        symbol: Symbol = f.Symbol
                        ticker: str = symbol.Value
        
                        # calculate slope
                        if ticker in self.tickers:
                            cds_1y: int = cds_1y_data[ticker]
                            cds_5y: int = cds_5y_data[ticker]
                            slope[symbol] = cds_5y - cds_1y
        
        if not self.selection_flag:
            return Universe.Unchanged
        
        last_update_date_1y: datetime.date = data_tools.EquityCDS1Y.get_last_update_date()
        last_update_date_5y: datetime.date = data_tools.EquityCDS5Y.get_last_update_date()
        if (self.Securities[self.cds_1y].GetLastData() and last_update_date_1y < self.Time.date() or
            self.Securities[self.cds_5y].GetLastData() and last_update_date_5y < self.Time.date()):
            return []
        if len(slope) >= self.quantile:
            sorted_by_slope: List[Symbol] = sorted(slope, key=slope.get, reverse=True)
            quantile: int = int(len(sorted_by_slope) / self.quantile)
            self.long_symbols = sorted_by_slope[-quantile:]
            self.short_symbols = sorted_by_slope[:quantile]
        return self.long_symbols + self.short_symbols
    
    def OnData(self, slice: Slice) -> None:
        if not self.selection_flag: 
            return
        self.selection_flag = False
        
        # trade execution
        targets: List[PortfolioTarget] = []
        for i, portfolio in enumerate([self.long_symbols, self.short_symbols]):
            for symbol in portfolio:
                if slice.ContainsKey(symbol) and slice[symbol] is not None:
                    targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
        self.SetHoldings(targets, True)
        self.long_symbols.clear()
        self.short_symbols.clear()
    def Selection(self) -> None:
        self.selection_flag = True




发表评论

了解 Quant Buffet 的更多信息

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

继续阅读