
策略涉及根据纳尔逊-西格尔收益率曲线的曲率因子对G10货币进行排序,做空低曲率货币并买入高曲率货币,每月进行再平衡,并调整投资组合规模。
资产类别: 差价合约,远期,期货,掉期 | 地区: 全球 | 周期: 每月 | 市场: 外汇 | 关键词: 曲率因子
I. 策略概要
投资范围包括G10货币。该策略首先计算Nelson-Siegel收益率曲线因子,特别关注曲率因子(Ct),这对于交易策略至关重要。货币根据其相对曲率被分为不同的投资组合。曲率较低的货币被做空,而曲率较高的货币被买入。每个投资组合包含三种货币,并每月重新平衡。尽管论文中的投资组合基于三种货币,但也测试了使用一种、两种或四种货币的变体。该策略采用OLS方法,但通常使用非线性最小二乘法进行参数化。
II. 策略合理性
该论文强调,即使在控制了常见的汇率预测因子后,Nelson-Siegel收益率曲线的相对曲率因子对一到六个月的货币走势仍具有预测能力。与典型的套利交易不同,曲率交易不依赖于日元或瑞士法郎等传统融资货币,从而降低了崩溃风险敞口。无论其他全球或国家特定变量(如外汇波动性、流动性或商品价格)如何,基于曲率因子的汇率可预测性依然强劲。此外,汇率波动等传统定价因子无法在线性框架中解释曲率交易回报。曲率因子表明短期利率收敛到长期利率的速度,反映了货币政策的前瞻性立场。较高的曲率表明鹰派前景,而较低的曲率则指向更鸽派的政策立场。
III. 来源论文
From Carry Trades to Curvy Trades [点击查看论文]
- Ferdinand Dreher, Johannes Gräb和Thomas Kostka,格罗宁根大学,欧洲中央银行(ECB),欧洲中央银行(ECB)
<摘要>
传统的套利交易策略基于短期利率差异,忽略了收益率曲线中嵌入的任何其他信息。我们推导了G10货币之间套利交易投资组合的回报分布,其中买卖货币的信号基于收益率曲线的汇总度量,即Nelson-Siegel因子。我们发现,基于相对曲率因子(即曲线交易)的策略比传统套利交易策略产生更高的夏普比率和更小的回报偏度。曲线交易较少依赖于典型的套利货币,如日元和瑞士法郎,因此受崩溃风险的影响较小。与此相符的是,传统套利交易回报的标准定价因子(如汇率波动率)无法在线性资产定价框架中解释曲线交易回报。我们的发现与近期对曲率因子的解释一致。相对较高的曲率预示着中期未来短期利率路径相对较高,从而对货币构成上行压力。


IV. 回测表现
| 年化回报 | 2.64% |
| 波动率 | 6.7% |
| β值 | 0.033 |
| 夏普比率 | 0.39 |
| 索提诺比率 | -0.572 |
| 最大回撤 | N/A |
| 胜率 | 51% |
V. 完整的 Python 代码
import data_tools
from typing import List, Dict
from AlgorithmImports import *
class CurvatureFactorInCurrencies(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.data:Dict[Symbol, SymbolData] = {}
self.period:int = 21
self.number_of_currencies:int = 3
self.leverage:int = 5
# Symbols - Currency futures, 10Y bond yield, 5Y bond yield, cash rate data.
# Cash rate source: https://www.quandl.com/data/OECD-Organisation-for-Economic-Co-operation-and-Development
self.symbols:List[str] = [
('CME_AD1', 'AU10YT', 'AU5YT', 'IR3TIB01AUM156N'), # Australian Dollar Futures, Continuous Contract #1
('CME_CD1', 'CA10YT', 'CA5YT', 'IR3TIB01CAM156N'), # Canadian Dollar Futures, Continuous Contract #1
('CME_SF1', 'CH10YT', 'CH5YT', 'IR3TIB01CHM156N'), # Swiss Franc Futures, Continuous Contract #1
('CME_EC1', 'DE10YT', 'DE5YT', 'IR3TIB01EZM156N'), # Euro FX Futures, Continuous Contract #1
('CME_BP1', 'GB10YT', 'GB5YT', 'LIOR3MUKM'), # British Pound Futures, Continuous Contract #1
('CME_JY1', 'JP10YT', 'JP5YT', 'IR3TIB01JPM156N'), # Japanese Yen Futures, Continuous Contract #1
('CME_NE1', 'NZ10YT', 'NZ5YT', 'IR3TIB01NZM156N'), # New Zealand Dollar Futures, Continuous Contract #1
('CME_MP1', 'MX10YT', 'MX5YT', 'IR3TIB01MXM156N') # Mexican Peso Futures, Continuous Contract #1
]
for currency_future, bond_yield_symbol_10, bond_yield_symbol_5, cash_rate_symbol in self.symbols:
# Currency futures data.
data = self.AddData(data_tools.QuantpediaFutures, currency_future, Resolution.Daily)
data.SetFeeModel(data_tools.CustomFeeModel())
data.SetLeverage(self.leverage)
# Bond yield data.
self.AddData(data_tools.QuantpediaBondYield, bond_yield_symbol_10, Resolution.Daily)
self.AddData(data_tools.QuantpediaBondYield, bond_yield_symbol_5, Resolution.Daily)
# Interbank rate data.
self.AddData(data_tools.InterestRate3M, cash_rate_symbol, Resolution.Daily)
for symbol_tuple in self.symbols:
self.data[symbol_tuple[0]] = data_tools.SymbolData(1) # Need to check data for trading
for symbol in symbol_tuple[1:]:
self.data[symbol] = data_tools.SymbolData(self.period)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.current_month:int = -1
def OnData(self, data:Slice) -> None:
for symbol_tuple in self.symbols:
for symbol in symbol_tuple:
if symbol in data and data[symbol]:
price:float = data[symbol].Value
self.data[symbol].update(price)
if self.Time.month == self.current_month:
return
self.current_month = self.Time.month
ir_last_update_date:Dict[str, datetime.date] = data_tools.InterestRate3M.get_last_update_date()
qp_futures_last_update_date:Dict[str, datetime.date] = data_tools.QuantpediaFutures.get_last_update_date()
bond_last_update_date:Dict[str, datetime.date] = data_tools.QuantpediaBondYield.get_last_update_date()
curvature = {}
for currency_future, bond_yield_symbol_10, bond_yield_symbol_5, cash_rate_symbol in self.symbols:
# check if data is still coming
if (self.Securities[currency_future].GetLastData() and qp_futures_last_update_date[currency_future] > self.Time.date()) \
and (self.Securities[cash_rate_symbol].GetLastData() and ir_last_update_date[cash_rate_symbol] > self.Time.date()) \
and (self.Securities[bond_yield_symbol_10].GetLastData() and bond_last_update_date[bond_yield_symbol_10] > self.Time.date()) \
and (self.Securities[bond_yield_symbol_5].GetLastData() and bond_last_update_date[bond_yield_symbol_5] > self.Time.date()):
if self.data[currency_future].is_ready() and self.data[bond_yield_symbol_10].is_ready() \
and self.data[bond_yield_symbol_5].is_ready() and self.data[cash_rate_symbol].is_ready():
# curvature = (5y - 3m) - (10y - 5y)
first_bracket:float = self.data[bond_yield_symbol_5].performance() - self.data[cash_rate_symbol].performance()
second_bracket:float = self.data[bond_yield_symbol_10].performance() - self.data[bond_yield_symbol_5].performance()
curvature[currency_future] = first_bracket - second_bracket
long:List[Symbol] = []
short:List[Symbol] = []
if len(curvature) >= self.number_of_currencies * 2:
sorted_by_curvature:List[Symbol] = [x[0] for x in sorted(curvature.items(), key=lambda item: item[1], reverse=True)]
long = sorted_by_curvature[:self.number_of_currencies]
short = sorted_by_curvature[-self.number_of_currencies:]
currencies_invested:List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in currencies_invested:
if symbol not in long + short:
self.Liquidate(symbol)
for symbol in long:
if symbol in data and data[symbol]:
self.SetHoldings(symbol, 1 / len(long))
for symbol in short:
if symbol in data and data[symbol]:
self.SetHoldings(symbol, -1 / len(short))