
“通过国民净资产与GDP比率和外币债务每月交易55种货币,做多国民净资产与GDP比率低/外币债务高的货币,做空国民净资产与GDP比率高/外币债务低的货币,使用等权重投资组合。”
资产类别: 差价合约、远期、期货 | 地区: 全球 | 周期: 每月 | 市场: 外汇 | 关键词: 货币因子
I. 策略概要
投资范围包括55个国家的货币。在月末,货币根据其国民净资产(NFA)与GDP的比率分为两组。在低NFA组中,做多外币计价债务份额最高的40%国家。在高NFA组中,做空此类债务份额最低的40%国家。投资组合等权重,每月重新平衡,利用国民净资产头寸与外币债务敞口之间的关系来指导交易决策。
II. 策略合理性
研究表明,在金融困境时期(即金融家吸收能力非常低,风险厌恶情绪非常高),净债务国面临货币贬值。因此,投资者要求持有债务国货币的溢价,因为它们风险更高。此外,Eichengreen和Hausmann(2005)认为,货币风险溢价的程度也与外债的货币计价有关,因为面临政治不稳定或高通胀的风险较高国家可能被迫发行以外币计价的债务。
III. 来源论文
Currency Premia and Global Imbalances [点击查看论文]
- Pasquale Della Corte, Steven J. Riddiough, Lucio Sarno
<摘要>
我们表明,一个全球失衡风险因子,它捕捉了国家外部失衡的利差及其以外币发行外部负债的倾向,解释了货币超额回报的横截面变异。经济直觉很简单:净债务国提供货币风险溢价,以补偿愿意为负外部失衡提供资金的投资者,因为它们的货币在经济不景气时会贬值。这种机制与基于不完善金融市场资本流动的汇率理论一致。我们还发现,全球失衡因子在其他主要资产市场的横截面中得到了定价。


IV. 回测表现
| 年化回报 | 4.4% |
| 波动率 | 6.43% |
| β值 | -0.048 |
| 夏普比率 | 0.68 |
| 索提诺比率 | -0.365 |
| 最大回撤 | -20% |
| 胜率 | 35% |
V. 完整的 Python 代码
from AlgorithmImports import *
import data_tools
#endregion
class GlobalImbalanceCurrencyFactor(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.quantpedia_futures:bool = False
self.countries:Dict[str, List[str]] = {
'Australia': ['AUDUSD', 'AUS_GDP'],
'United Kingdom': ['GBPUSD', 'GBR_GDP'],
'New Zealand': ['NZDUSD', 'NZL_GDP'],
'Canada': ['USDCAD', 'CAN_GDP'],
'Switzerland': ['USDCHF', 'CHE_GDP'],
'China': ['USDCNH', 'CHN_GDP'],
'Czech Republic': ['USDCZK', 'CZE_GDP'],
'Denmark': ['USDDKK', 'DNK_GDP'],
'Hungary': ['USDHUF', 'HUN_GDP'],
'India': ['USDINR', 'IND_GDP'],
'Japan': ['USDJPY' ,'JPN_GDP'],
'Norway': ['USDNOK', 'NOR_GDP'],
'Poland': ['USDPLN', 'POL_GDP'],
'Saudi Arabia': ['USDSAR', 'SAU_GDP'],
'Sweden': ['USDSEK', 'SWE_GDP'],
'Thailand': ['USDTHB', 'THA_GDP'],
'Turkey': ['USDTRY', 'TUR_GDP']
}
if self.quantpedia_futures:
self.countries:Dict[str, List[str]] = {
'Australia': ['CME_AD1', 'AUS_GDP'],
'United Kingdom': ['CME_BP1', 'GBR_GDP'],
'New Zealand': ['CME_NE1', 'NZL_GDP'],
'Canada': ['CME_CD1', 'CAN_GDP'],
'Switzerland': ['CME_SF1', 'CHE_GDP'],
'Japan': ['CME_JY1' ,'JPN_GDP']
}
self.quantile:int = 2
self.leverage:int = 5
self.net_foreign_assets:Symbol = self.AddData(data_tools.QuantpediaNetForeignAssets, 'NET_FOREIGN_ASSETS', Resolution.Daily).Symbol
self.debt_all_currencies:Symbol = self.AddData(data_tools.QuantpediaCurrenciesDebt ,'DEBT_ALL_CURRENCIES', Resolution.Daily).Symbol
self.debt_foreign_currencies:Symbol = self.AddData(data_tools.QuantpediaCurrenciesDebt ,'DEBT_FOREIGN_CURRENCIES', Resolution.Daily).Symbol
# store SymbolData object under country
self.data:Dict[str, SymbolData] = { x : SymbolData() for x in self.countries }
for country in self.countries:
ticker:str = self.countries[country][0]
gdp:str = self.countries[country][1]
if self.quantpedia_futures:
data = self.AddData(data_tools.QuantpediaFutures, ticker, Resolution.Daily)
else:
# subscribe to currency
data = self.AddForex(ticker, Resolution.Daily, Market.Oanda)
data.SetLeverage(self.leverage)
# subscribe to quandl gdp
self.AddData(data_tools.GDPData, gdp, Resolution.Daily)
self.symbol:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.selection_flag:bool = False
self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)
def OnData(self, data: Slice) -> None:
if self.debt_all_currencies in data and data[self.debt_all_currencies]:
for country in self.countries:
# get country debt based on index from data object
debt = data[self.debt_all_currencies].GetProperty(country)
# store debt in SymbolData object under country name in dictionary
self.data[country].all_currencies_debt_value = float(debt)
if self.net_foreign_assets in data and data[self.net_foreign_assets]:
for country in self.countries:
# get country net foreign asset based on index from data object
asset = data[self.net_foreign_assets].GetProperty(country)
# store net foreign asset in SymbolData object under country name in dictionary
self.data[country].nfa_value = float(asset)
if self.debt_foreign_currencies in data and data[self.debt_foreign_currencies]:
for country in self.countries:
# get country debt based on index from data object
debt = data[self.debt_foreign_currencies].GetProperty(country)
# store debt in SymbolData object under country name in dictionary
self.data[country].foreign_currencies_debt_value = float(debt)
debt_last_update_date:Dict[str, datetime.date] = data_tools.QuantpediaCurrenciesDebt.get_last_update_date()
nfa_last_update_date:Dict[str, datetime.date] = data_tools.QuantpediaNetForeignAssets.get_last_update_date()
gdp_last_update_date:Dict[str, datetime.date] = data_tools.GDPData.get_last_update_date()
if self.quantpedia_futures:
futures_last_update_date:Dict[str, datetime.date] = data_tools.QuantpediaFutures.get_last_update_date()
# custom data is still comming in
if not all(self.Securities[x].GetLastData() and self.Time.date() < debt_last_update_date[x.Value] for x in [self.debt_foreign_currencies, self.debt_all_currencies]):
self.Liquidate()
return
if not (self.Securities[self.net_foreign_assets].GetLastData() and self.Time.date() < nfa_last_update_date):
self.Liquidate()
return
for country in self.countries:
ticker:str = self.countries[country][0]
gdp:str = self.countries[country][1]
if gdp in data and data[gdp]:
value = data[gdp].Value
# update gdp value
self.data[country].gdp_value = value
self.data[country].gdp_ready = False
# rebalance at the end of the month
if not self.selection_flag:
return
self.selection_flag = False
# storing nfa/gdp values under countries
nfa:Dict[str, float] = {}
# storing foreign/all gross debts under countries
share_of_debt:Dict[str, float] = {}
for country, symbol_data in self.data.items():
# data is still comming in
if self.quantpedia_futures:
if self.Securities[self.countries[country][0]].GetLastData() and self.Time.date() > futures_last_update_date[self.countries[country][0]]:
continue
else:
if not self.Securities[self.countries[country][0]].GetLastData():
continue
if self.Securities[self.countries[country][1]].GetLastData() and self.Time.date() > gdp_last_update_date[self.countries[country][1]]:
continue
if not symbol_data.is_ready():
continue
# store value under country
nfa[country] = symbol_data.nfa_value / symbol_data.gdp_value
# get country debts
foreign_debt:float = symbol_data.foreign_currencies_debt_value
all_debt:float = symbol_data.all_currencies_debt_value
# can't perform division by zero
if all_debt == 0:
share_of_debt[country] = 0
continue
# store share of debt under country
share_of_debt[country] = foreign_debt / all_debt
# performing one month lag
for _, symbol_data in self.data.items():
symbol_data.gdp_ready = True
if len(nfa) < self.quantile:
self.Liquidate()
return
quantile:int = int(len(nfa) / self.quantile)
sorted_nfa:List[str] = [x[0] for x in sorted(nfa.items(), key=lambda item: item[1])]
sorted_share_of_debt:List[str] = [x[0] for x in sorted(share_of_debt.items(), key=lambda item: item[1])]
low_nfa:List[str] = sorted_nfa[:quantile]
high_nfa:List[str] = sorted_nfa[-quantile:]
low_nfa_by_share_of_debt:List[str] = sorted(low_nfa, key = lambda x: share_of_debt[x])
high_nfa_by_share_of_debt:List[str] = sorted(high_nfa, key = lambda x: share_of_debt[x])
long:List[str] = []
short:List[str] = []
if len(low_nfa_by_share_of_debt) >= self.quantile and len(high_nfa_by_share_of_debt) >= self.quantile:
quantile = int(len(low_nfa_by_share_of_debt) / self.quantile)
long = [self.countries[x][0] for x in low_nfa_by_share_of_debt[-quantile:]]
short = [self.countries[x][0] for x in high_nfa_by_share_of_debt[:quantile]]
# trade execution
long_length:int = len(long)
short_length:int = len(short)
invested:List[str] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
for ticker in invested:
if ticker not in long + short:
self.Liquidate(ticker)
for ticker in long:
weight = 1 / long_length
if ticker[:3] == 'USD':
weight = weight * -1
self.SetHoldings(ticker, weight)
for ticker in short:
weight = -1 / short_length
if ticker[:3] == 'USD':
weight = weight * -1
self.SetHoldings(ticker, weight)
def Selection(self) -> None:
self.selection_flag = True
class SymbolData():
def __init__(self):
self.nfa_value = -1
self.all_currencies_debt_value = -1
self.foreign_currencies_debt_value = -1
self.gdp_value = -1
self.gdp_ready = False
def is_ready(self):
return self.gdp_ready and self.nfa_value != -1 and \
self.all_currencies_debt_value != -1 and self.foreign_currencies_debt_value != -1