Quant BuffetRelax, Not Over Thinking

Sovereign CDS Predicts FX Market Return

Log in to collect

Academic 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