Sovereign CDS Predicts FX Market Return
Log in to collectAcademic paper
Strategy in a nutshell
Trades 29 USD currency pairs monthly, ranking them by sovereign CDS term premia. Goes long on currencies with the highest CDS premia and short on those with the lowest, using equally weighted portfolios rebalanced monthly.
Economic rationale
Currencies behave like financial assets: the slope of sovereign CDS spreads captures country-specific credit risk changes. Positive CDS slope innovations predict currency appreciation, enabling a cross-sectional trading strategy robust to global crises.
Backtest performance
Annualised return4.84%
Volatility5.92%
Beta0.041
Sharpe ratio0.82
Win rate50%
Full Python code
from AlgorithmImports import *
import numpy as np
#endregion
class SovereignCDSPredictsFXMarketReturn(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2008, 1, 1)
self.SetCash(100000)
# forex pair symbol : (CDS country symbol, long-short switch position flag)
self.symbols:Dict[str, Tuple[List[str], bool]] = {
'AUDUSD' : (['AU'], False),
'EURUSD' : (['ES', 'IT', 'GR'], False),
'GBPUSD' : (['GB'], False),
'USDTRY' : (['TR'], True),
'RUBUSD' : (['RU'], False),
'BRLUSD' : (['BR'], False),
# 'USDCAD' : (['CA'], True),
# 'USDMXN' : (['MX'], True),
}
self.cds_symbols:Dict[str, tuple] = {}
for fx_symbol, (country_codes, _) in self.symbols.items():
# subscribe forex symbol
data:Forex = self.AddForex(fx_symbol, Resolution.Minute, Market.Oanda)
data.SetLeverage(5)
# subscribe CDS symbols
for country_code in country_codes:
cds_1y_symbol:Symbol = self.AddData(CDSData1Y, country_code, Resolution.Daily).Symbol
cds_10y_symbol:Symbol = self.AddData(CDSData10Y, country_code, Resolution.Daily).Symbol
self.cds_symbols[country_code] = (cds_1y_symbol, cds_10y_symbol)
self.recent_month:int = -1
self.quantile:int = 3
def OnData(self, data:Slice) -> None:
if self.Time.month == self.recent_month:
return
self.recent_month = self.Time.month
# end of custom data
last_update_date_1Y:Dict[str, datetime.date] = CDSData1Y.get_last_update_date()
last_update_date_10Y:Dict[str, datetime.date] = CDSData10Y.get_last_update_date()
# store actual CDS
actual_cds:Dict[str, float] = {}
for fx_symbol, (country_codes, _) in self.symbols.items():
# price data are available
if fx_symbol in data and data[fx_symbol]:
cds_values:List[float] = []
for country_code in country_codes:
# CDS data are available
if self.Securities[self.cds_symbols[country_code][0]].GetLastData() and self.Securities[self.cds_symbols[country_code][1]].GetLastData():
if self.Time.date() <= last_update_date_1Y[self.cds_symbols[country_code][0]] and self.Time.date() <= last_update_date_10Y[self.cds_symbols[country_code][1]]:
# get most recent CDS values
cds_1y:float = np.log(self.Securities[self.cds_symbols[country_code][0]].Price)
cds_10y:float = np.log(self.Securities[self.cds_symbols[country_code][1]].Price)
cds_values.append(cds_10y - cds_1y)
if len(cds_values) != 0:
actual_cds[fx_symbol] = np.mean(cds_values)
if len(actual_cds) < self.quantile:
self.Liquidate()
return
# sort by CDS
sorted_by_cds:List = sorted(actual_cds.items(), key = lambda x: x[1], reverse=True)
quantile:int = int(len(sorted_by_cds) / self.quantile)
# long the highest CDS portfolio and short the lowest CDS portfolio
long:List[str] = [x[0] for x in sorted_by_cds[:quantile]]
short:List[str] = [x[0] for x in sorted_by_cds[-quantile:]]
long_c:int = len(long)
short_c:int = len(short)
# liquidate
invested:List[str] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in long + short:
self.Liquidate(symbol)
# EW portfolio
for symbol in long:
# long-short swap position flag
ls_switch:bool = self.symbols[symbol][1]
if not ls_switch:
self.SetHoldings(symbol, 1 / long_c)
else:
self.SetHoldings(symbol, -1 / long_c)
for symbol in short:
# long-short swap position flag
ls_switch:bool = self.symbols[symbol][1]
if not ls_switch:
self.SetHoldings(symbol, -1 / short_c)
else:
self.SetHoldings(symbol, 1 / short_c)
# 1Y Credit Default Swap data.
# Source: https://www.investing.com/search/?q=CDS%205%20years&tab=quotes
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class CDSData1Y(PythonData):
def GetSource(self, config: SubscriptionDataConfig, date: datetime, isLiveMode: bool) -> SubscriptionDataSource:
return SubscriptionDataSource("data.quantpedia.com/backtesting_data/cds/{0}_CDS_1Y.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
_last_update_date:Dict[str, datetime.date] = {}
@staticmethod
def get_last_update_date() -> Dict[str, datetime.date]:
return CDSData1Y._last_update_date
def Reader(self, config: SubscriptionDataConfig, line: str, date: datetime, isLiveMode: bool) -> BaseData:
data = CDSData1Y()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(';')
data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
# store last date of the symbol
if data.Symbol not in CDSData1Y._last_update_date:
CDSData1Y._last_update_date[data.Symbol] = datetime(1,1,1).date()
if data.Time.date() > CDSData1Y._last_update_date[data.Symbol]:
CDSData1Y._last_update_date[data.Symbol] = data.Time.date()
data.Value = float(split[1])
return data
# 10Y Credit Default Swap data.
# Source: https://www.investing.com/
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class CDSData10Y(PythonData):
def GetSource(self, config: SubscriptionDataConfig, date: datetime, isLiveMode: bool) -> SubscriptionDataSource:
return SubscriptionDataSource("data.quantpedia.com/backtesting_data/cds/{0}_CDS_10Y.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
_last_update_date:Dict[str, datetime.date] = {}
@staticmethod
def get_last_update_date() -> Dict[str, datetime.date]:
return CDSData10Y._last_update_date
def Reader(self, config: SubscriptionDataConfig, line: str, date: datetime, isLiveMode: bool) -> BaseData:
data = CDSData10Y()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(';')
data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
# store last date of the symbol
if data.Symbol not in CDSData10Y._last_update_date:
CDSData10Y._last_update_date[data.Symbol] = datetime(1,1,1).date()
if data.Time.date() > CDSData10Y._last_update_date[data.Symbol]:
CDSData10Y._last_update_date[data.Symbol] = data.Time.date()
data.Value = float(split[1])
return data