Quant Buffet放轻松,别过度思虑

货币中的曲率因子

登录后收藏

学术论文

From Carry Trades to Curvy Trades

作者From Carry Trades to Curvy Trades [点击查看论文]

机构
  • NLUniversity of Groningen
  • DEEuropean Central Bank
  • ?European Central Bank (ECB)

策略概要

投资范围包括G10货币。该策略首先计算Nelson-Siegel收益率曲线因子,特别关注曲率因子(Ct),这对于交易策略至关重要。货币根据其相对曲率被分为不同的投资组合。曲率较低的货币被做空,而曲率较高的货币被买入。每个投资组合包含三种货币,并每月重新平衡。尽管论文中的投资组合基于三种货币,但也测试了使用一种、两种或四种货币的变体。该策略采用OLS方法,但通常使用非线性最小二乘法进行参数化。

II. 策略合理性

该论文强调,即使在控制了常见的汇率预测因子后,Nelson-Siegel收益率曲线的相对曲率因子对一到六个月的货币走势仍具有预测能力。与典型的套利交易不同,曲率交易不依赖于日元或瑞士法郎等传统融资货币,从而降低了崩溃风险敞口。无论其他全球或国家特定变量(如外汇波动性、流动性或商品价格)如何,基于曲率因子的汇率可预测性依然强劲。此外,汇率波动等传统定价因子无法在线性框架中解释曲率交易回报。曲率因子表明短期利率收敛到长期利率的速度,反映了货币政策的前瞻性立场。较高的曲率表明鹰派前景,而较低的曲率则指向更鸽派的政策立场。

回测表现

波动率6.7%
夏普比率0.39
索提诺比率-0.572
胜率51%

完整 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))