Quant Buffet放轻松,别过度思虑

信用违约掉期(CDS)期限结构预测股票收益

登录后收藏

学术论文

Term Structure of Credit Default Swap Spreads and Cross-Section of Stock Returns

作者作者:Han; 机构:多伦多大学罗特曼管理学院(University of Toronto

机构
  • ?Zhou
  • ?Rotman School of Management),旧金山州立大学
论文摘要

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

策略概要

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

策略合理性

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

回测表现

年化收益27.57%
波动率23.84%
贝塔0.166
夏普比率0.99
索提诺比率-0.096
胜率50%

完整 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