股票与信用违约掉期(CDS)动量组合
登录后收藏学术论文
Jongsub Lee; Stace Sirmans
- KRSeoul National University
- University of Florida
- ?University of Florida - Warrington College of Business Administration
- Auburn University
策略概要
该投资策略结合了联合动量和分离的逆向信号,使用在纽约证券交易所、美国证券交易所和纳斯达克上市的公司的股票和CDS合约。股票根据过去12个月和4个月的回报率分为五分位数,CDS合约也进行类似排序。联合动量策略做多股票回报率和CDS均位于顶部五分位数的公司,做空均位于底部五分位数的公司。逆向策略买入股票位于底部五分位数而CDS位于顶部的公司,反之亦然。投资组合按价值加权,持有期为一个月。
II. 策略合理性
投资范围包括在纽约证券交易所、美国证券交易所和纳斯达克上市的公司,股票回报数据来自CRSP数据库,CDS合约数据来自Markit Group。该策略使用两个信号:联合动量和分离的逆向。在联合动量中,股票和CDS根据过去12个月和4个月的回报率分为五分位数。投资者做多顶部五分位数的股票和CDS,做空底部五分位数。在分离的逆向中,买入底部五分位数的股票,卖空顶部五分位数的CDS。投资组合按价值加权,持有期为一个月。
回测表现
波动率24.09%
夏普比率0.81
索提诺比率0.109
胜率49%
完整 Python 代码
from AlgorithmImports import *
import data_tools
from typing import List, Dict
#endregion
class CombinedStockandCDSMomentum(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2005, 1, 1)
self.SetCash(100_000)
self.stock_period: int = 12 * 21
self.cds_period: int = 4 * 21
self.quantile: int = 5
self.leverage: int = 5
market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.cds: Symbol = self.AddData(data_tools.EquityCDS5Y, 'CDS', Resolution.Daily).Symbol
# data yet to be initialized
self.tickers: List[str] = [] # CDS universe tickers
self.data: Dict[str, data_tools.SymbolData] = {} # equity symbol data
self.quantity: Dict[Symbol, float] = {} # traded monthly quantity
self.weight: Dict[Symbol, float] = {}
self.selection_flag: bool = False
self.UniverseSettings.Leverage = self.leverage
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.settings.daily_precise_end_time = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
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]:
# Update the rolling window every day.
if self.Securities.ContainsKey(self.cds):
cds_data = self.Securities[self.cds].GetLastData()
if cds_data:
# data has not been initialized yet
if len(self.data) == 0:
self.tickers = list([x.upper() for x in cds_data.GetStorageDictionary().Keys])
self.data = { x : data_tools.SymbolData(self.stock_period, self.cds_period) for x in self.tickers }
for stock in fundamental:
ticker: str = stock.Symbol.Value
# Store daily price and cds.
if ticker in self.data:
cds_price: float = cds_data[ticker]
self.data[ticker].update(stock.AdjustedPrice, cds_price)
if not self.selection_flag:
return Universe.Unchanged
# cds data probably ended
custom_data_last_update_date: datetime.date = data_tools.EquityCDS5Y.get_last_update_date()
if self.Securities[self.cds].GetLastData() and self.Time.date() > custom_data_last_update_date:
return Universe.UNCHANGED
selected: List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Symbol.Value in self.tickers]
market_cap: Dict[Symbol, float] = {}
price_momentum: Dict[Symbol, float] = {}
cds_momentum: Dict[Symbol, float] = {}
for stock in selected:
symbol: Symbol = stock.Symbol
ticker: str = symbol.Value
if not self.data[ticker].is_ready():
continue
if stock.MarketCap == 0:
continue
market_cap[symbol] = stock.MarketCap
price_momentum[symbol] = self.data[ticker].price_momentum()
cds_momentum[symbol] = self.data[ticker].cds_momentum()
if len(price_momentum) > self.quantile:
sorted_by_price_momentum: List[Symbol] = [x[0] for x in sorted(price_momentum.items(), key=lambda item:item[1], reverse=True)]
quantile: int = int(len(sorted_by_price_momentum) / self.quantile)
top_by_momentum: List[Symbol] = sorted_by_price_momentum[:quantile]
bottom_by_momentum: List[Symbol] = sorted_by_price_momentum[-quantile:]
sorted_by_cds_momentum: List[Symbol] = [x[0] for x in sorted(cds_momentum.items(), key=lambda item:item[1], reverse=True)]
quantile: int = int(len(sorted_by_cds_momentum) / self.quantile)
top_by_cds_momentum: List[Symbol] = sorted_by_cds_momentum[:quantile]
bottom_by_cds_momentum: List[Symbol] = sorted_by_cds_momentum[-quantile:]
# Joint momentum
joint_long: List[Symbol] = [x for x in top_by_momentum if x in top_by_cds_momentum]
joint_short: List[Symbol] = [x for x in bottom_by_momentum if x in bottom_by_cds_momentum]
# Contrarian strategy
contrarian_long: List[Symbol] = [x for x in bottom_by_momentum if x in top_by_cds_momentum]
contrarian_short: List[Symbol] = [x for x in top_by_momentum if x in bottom_by_cds_momentum]
# Strategy weighting
portfolio_weight: float = 0.5 # two-strategy portfolio adjustment
for i, portfolio in enumerate([[joint_long, contrarian_long], [joint_short, contrarian_short]]):
for subportfolio in portfolio:
mc_sum: float = sum(list(map(lambda x: market_cap[x], subportfolio)))
for symbol in subportfolio:
w: float = ((-1)**i) * (market_cap[symbol] / mc_sum) * portfolio_weight
q: float = (self.Portfolio.TotalPortfolioValue * w) / self.data[symbol.Value].price[0]
self.quantity[symbol] = q
return list(self.quantity.keys())
def OnData(self, slice: Slice) -> None:
if not self.selection_flag:
return
self.selection_flag = False
self.Liquidate()
for symbol, q in self.quantity.items():
if slice.contains_key(symbol) and slice[symbol]:
self.MarketOrder(symbol, q)
self.quantity.clear()
def Selection(self) -> None:
self.selection_flag = True