投资宇宙由32种货币组成,数据来自汤森路透和Markit。每月,投资者根据过去1个月的主权CDS利差,将货币按五分位排序,标记主权风险级别(1为最低,5为最高)。构建多空价差投资组合,做多主权风险最低的五分位,做空最高的五分位。投资组合为等权重,并每月重新平衡。

策略概述

投资宇宙由32种货币组成(数据来自汤森路透)。用于计算的数据包括48个国家的月度最具流动性的5年期主权CDS利差(来自Markit)。每个月,投资者根据过去1个月的主权CDS利差水平,将货币按五分位投资组合进行排序,并标记从1(主权风险最低)到5(主权风险最高),然后计算货币组合在持有期为1个月的超额回报。接着,他们构建最终的多空价差投资组合,即做多主权风险最低的五分位组合,做空主权风险最高的五分位组合。投资组合为等权重,并每月重新平衡。

策略合理性

该策略利用了主权CDS利差与货币回报之间直接联系的发现。解释这一现象的原因是,在经济衰退的背景下(由异常的主权CDS期限结构作为代理),这种效应更加明显。我们的策略基于这样一个想法:主权CDS广泛用于衡量主权信用风险的可能性。策略有效的主要根本原因是,较高的国家主权CDS利差(即较高的主权信用风险)会导致该国货币贬值,从而导致货币对数回报的下降。主权动量效应对自由浮动的货币或允许较大波动幅度的货币更强,并且在经济衰退的情况下,这种效应更加明显(由异常的主权CDS期限结构作为代理)。

论文来源

Sovereign Momentum Currency Returns [点击浏览原文]

<摘要>

我们研究了主权信用风险横截面与货币现货价格之间的关系。我们发现,过去的主权信用风险(通过主权信用违约掉期(CDS)利差衡量)可以预测未来的货币现货回报。特别是,我们记录了最高和最低五分位主权CDS利差之间显著的货币投资组合价差,超出无风险回报率(年回报率高达9.4%)。这些结果表明了一种基于主权信用风险的全新盈利货币回报策略。

回测表现

年化收益率5%
波动率5.74%
Beta0.031
夏普比率0.87
索提诺比率N/A
最大回撤N/A
胜率53%

完整python代码

from AlgorithmImports import *
import numpy as np
#endregion

class SovereignCDSCurrencyFactor(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2008, 1, 1)
        self.SetCash(100000)
        
        # forex pair symbol : (CDS country symbol, GDP symbol, long-short switch position flag)
        self.symbols:Dict[str, Tuple[List[str], bool]] = {
            'AUDUSD' : (['AU'], False),
            'USDCAD' : (['CA'], True),
            'EURUSD' : (['ES', 'FR', 'IT', 'GR'], False),
            'GBPUSD' : (['GB'], False),
            'USDMXN' : (['MX'], True),
            'USDTRY' : (['TR'], True),
            'RUBUSD' : (['RU'], False),
            'BRLUSD' : (['BR'], False),
        }

        self.cds_symbols:Dict[str, tuple] = {}

        for fx_symbol, (country_codes, _) in self.symbols.items():
            # subscribe forex symbol
            data = self.AddForex(fx_symbol, Resolution.Minute, Market.Oanda)
            data.SetLeverage(5)
            
            # subscribe CDS symbols
            for country_code in country_codes:
                cds_symbol:Symbol = self.AddData(CDSData5Y, country_code, Resolution.Daily).Symbol
                self.cds_symbols[country_code] = cds_symbol
        
        self.quantile:int = 3
        self.recent_month:int = -1
        
    def OnData(self, data):
        if self.Time.month == self.recent_month:
            return
        self.recent_month = self.Time.month

        # store actual CDS
        actual_cds:Dict[str, float] = {}

        # end of custom data
        last_update_date_5Y:Dict[str, datetime.date] = CDSData5Y.get_last_update_date()

        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]].GetLastData():
                        if self.Time.date() <= last_update_date_5Y[self.cds_symbols[country_code]]:
                            # get most recent CDS value
                            cds:float = self.Securities[self.cds_symbols[country_code]].Price
                            cds_values.append(cds)
                
                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)
        
        # going long quintile Lowest SR and short quintile Highest SR
        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)

# 5Y 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 CDSData5Y(PythonData):
    def GetSource(self, config: SubscriptionDataConfig, date: datetime, isLiveMode: bool) -> SubscriptionDataSource:
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/cds/{0}_CDS_5Y.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 CDSData5Y._last_update_date

    def Reader(self, config: SubscriptionDataConfig, line: str, date: datetime, isLiveMode: bool) -> BaseData:
        data = CDSData5Y()
        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 CDSData5Y._last_update_date:
            CDSData5Y._last_update_date[data.Symbol] = datetime(1,1,1).date()
        if data.Time.date() > CDSData5Y._last_update_date[data.Symbol]:
            CDSData5Y._last_update_date[data.Symbol] = data.Time.date()

        data.Value = float(split[1])

        return data

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading