
The strategy trades 29 USD currency pairs, going long on currencies with high CDS term premia and short on low ones, with equally weighted portfolios rebalanced monthly.
ASSET CLASS: CFDs, futures | REGION: Global | FREQUENCY:
Monthly | MARKET: currencies | KEYWORD: Sovereign CDS
I. 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.
II. 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.
III. SOURCE PAPER
The Term Structure of Sovereign CDS and the Cross-Section Exchange Rate Predictability [Click to Open PDF]
Giovanni Calice and Ming Zeng.
<Abstract>
We provide novel evidence on exchange rate predictability by using the term premia of the sovereign credit default swap (CDS). Using a sample of 29 countries, we find that the sovereign CDS term premia significantly predict the exchange rate out-of-sample. On average, a steeper CDS spread curve for a country predicts its currency appreciation against the US dollar (USD). Empirically, while the sovereign CDS level mainly reflects global risk, the information in the term structure of the sovereign CDS spreads reveals country-specific risk. Notably, the predictive power of the term premia is robust after controlling for the sovereign CDS level and other conventional macroeconomic factors. Further analysis shows that the information in the sovereign CDS term structure is also helpful for forecasting other important financial markets such as the stock markets in different countries.
IV. BACKTEST PERFORMANCE
| Annualised Return | 4.84% |
| Volatility | 5.92% |
| Beta | 0.041 |
| Sharpe Ratio | 0.82 |
| Sortino Ratio | N/A |
| Maximum Drawdown | N/A |
| Win Rate | 50% |
V. 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
VI. Backtest Performance