投资范围包括10种流动性最强的货币,适合交易策略。日度即期和一个月远期汇率数据来自Datastream。动量形成期为一个月,使用平均远期贴水(AFD)、正交化收益分散度(ORD)和全球外汇波动率(VOL)。每月末按等权重重新平衡投资组合,满足特定信号时做多或做空动量组合。

策略概述

投资范围包括10种流动性最强的货币,这些货币非常适合应用于此类交易策略。(日度即期汇率和一个月远期汇率相对于美元的汇率数据可以从Datastream获取。)

我们采用一个月作为动量的形成期,并使用三个信号,这些信号需要为每种货币计算:AFD 是平均远期贴水 [方程 (5)],ORD 是正交化的收益分散度 [方程 (6)],VOL 是全球外汇波动率 [方程 (7)]。

根据表1中的条件 (f) 构建条件性动量投资组合: 交易规则如下: a) 当 AFD_t 为正、ORD_t 低(如果 ORD_t 高于过去36个月窗口中ORD的90百分位则为高ORD状态),且 VOL_t 低(如果 VOL_t 高于过去36个月窗口中VOL的90百分位则为高VOL状态)时,投资者应在传统的 [价差] 动量投资组合中做多(买入最高动量投资组合P5,卖出最低动量投资组合P1)。 b) 如果这三个条件中的任意一个未满足,投资者应在传统的动量 [价差] 投资组合中做空(卖出P5,买入P1)。

投资者应在每个月底对投资组合进行重新平衡,并等权配置。

策略合理性

动量是跨资产类别中最受欢迎的投资策略之一,不仅被货币投资者广泛采用。然而,自全球金融危机(GFC)以来,动量策略并未产生正的平均回报。货币市场在GFC后出现的重大变化是美元需求的增强。为了利用有关美元需求的信息,建议使用不同的指标,例如 AFD [由 Lustig 等人(2014年)提出]、ORD 或 VOL 以及它们的组合作为信号,以增强货币动量策略。本文表明,这些方法从提高回报率和优化夏普比率的角度来看非常具有吸引力。收益分散度(ORD)的增加导致了货币超额收益的回归,而波动性则有助于增强股票市场动量策略和货币套利交易。这三个信号提高了货币动量组合的表现,尤其是在短期动量组合中效果显著。值得一提的是,该策略依赖于美元的特殊地位,因此在采用其他基准货币时无法发挥作用。

论文来源

Conditional Currency Momentum Portfolios [点击浏览原文]

<摘要>

由于美元需求的强劲,货币动量投资组合自全球金融危机以来未能产生正收益。我们提出了结合平均远期贴水、货币市场波动率和货币组合收益分散度的条件性货币动量策略。我们的策略只有在平均远期贴水为正、波动率低且收益分散度低时才在动量投资组合中做多。我们发现,条件性一个月货币动量组合将夏普比率提高了0.69,年化确定等价回报提高了6.6%。

回测表现

年化收益率5.03%
波动率9.59%
Beta0.027
夏普比率0.52
索提诺比率-0.291
最大回撤N/A
胜率39%

完整python代码

import data_tools
import numpy as np
import pandas as pd
from AlgorithmImports import *
from pandas.core.frame import DataFrame
from typing import List, Dict
# endregion

class ConditionalCurrencyMomentumPortfolios(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)

        self.us_ir:Symbol = self.AddData(data_tools.InterestRate3M, 'IR3TIB01USM156N', Resolution.Daily).Symbol

        self.leverage:int = 5
        self.quantile:int = 4
        self.monthly_period:int = 36
        self.daily_period:int = 21

        self.data:Dict[Symbol, data_tools.SymbolData] = {}
        self.top_portfolio:List[Symbol] = []
        self.bottom_portfolio:List[Symbol] = []

        # Cash rate source: https://fred.stlouisfed.org/series/IR3TIB01USM156N
        self.symbols:Dict[str, str] = {
            "AUDUSD" : "IR3TIB01AUM156N",   # Australian Dollar Futures, Continuous Contract #1
            "GBPUSD" : "LIOR3MUKM",         # British Pound Futures, Continuous Contract #1
            "CADUSD" : "IR3TIB01CAM156N",   # Canadian Dollar Futures, Continuous Contract #1
            "EURUSD" : "IR3TIB01EZM156N",   # Euro FX Futures, Continuous Contract #1
            "JPYUSD" : "IR3TIB01JPM156N",   # Japanese Yen Futures, Continuous Contract #1
            "MXNUSD" : "IR3TIB01MXM156N",   # Mexican Peso Futures, Continuous Contract #1
            "NZDUSD" : "IR3TIB01NZM156N",   # New Zealand Dollar Futures, Continuous Contract #1
            "CHFUSD" : "IR3TIB01CHM156N"    # Swiss Franc Futures, Continuous Contract #1
        }
        
        # data subscription
        for symbol, rate_symbol in self.symbols.items():
            data:Security = self.AddForex(symbol, Resolution.Daily, Market.Oanda)
            data.SetFeeModel(data_tools.CustomFeeModel())
            data.SetLeverage(self.leverage)

            ir_symbol:Symbol = self.AddData(data_tools.InterestRate3M, rate_symbol, Resolution.Daily).Symbol

            self.data[data.Symbol] = data_tools.SymbolData(self.daily_period, ir_symbol)

        self.return_dispersion:RollingWindow = RollingWindow[float](self.monthly_period)
        self.volatility:RollingWindow = RollingWindow[float](self.monthly_period)

        self.SetWarmup(self.monthly_period * self.daily_period, Resolution.Daily)
        
        self.recent_month:int = -1
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.

    def OnData(self, data: Slice) -> None:
        rebalance_flag:bool = False

        # store daily prices
        for symbol, symbol_data in self.data.items():
            if symbol in data and data[symbol]:
                symbol_data.update_price(data[symbol].Price)

                # monthly rebalance
                if self.recent_month != self.Time.month and not rebalance_flag:
                    self.recent_month = self.Time.month
                    rebalance_flag = True

        if not rebalance_flag:
            return

        ir_last_update_date:Dict[str, datetime.date] = data_tools.InterestRate3M.get_last_update_date()
        curr_us_ir_value:Union[float, None] = self.Securities[self.us_ir].Price if self.Securities[self.us_ir].GetLastData() and ir_last_update_date[self.us_ir.Value] > self.Time.date() else None
        
        if curr_us_ir_value is None:
            self.Liquidate()
            return

        fd_by_symbol:Dict[Symbol, float] = {}

        # save daily prices lists
        for symbol, symbol_data in self.data.items():
            if self.Securities[symbol_data._ir_symbol].GetLastData():
                if ir_last_update_date[symbol_data._ir_symbol.Value] < self.Time.date():
                    continue
                    
                if not self.data[symbol].is_ready():
                    continue
                
                fd_by_symbol[symbol] = self.Securities[symbol_data._ir_symbol].Price - curr_us_ir_value

        self.return_dispersion.Add(np.sqrt((np.sum(([self.data[x].get_monthly_return() for x in fd_by_symbol] - np.mean([self.data[x].get_monthly_return() for x in fd_by_symbol])) ** 2) / (len(fd_by_symbol) - 1))))
        self.volatility.Add(np.mean(np.mean(np.abs([self.data[x].get_daily_returns() for x in fd_by_symbol]), axis=0)))
    
        if self.IsWarmingUp:
            return

        # signal calculation
        if len(fd_by_symbol) < self.quantile or (not self.return_dispersion.IsReady and not self.volatility.IsReady):
            self.Liquidate()
            return
        
        afd:float = np.mean(list(fd_by_symbol.values()))
        afd_signal:int = 1 if afd > 0 else 0
        ord_signal = 1 if self.return_dispersion[0] > np.percentile(np.array(list(self.return_dispersion)[1:]), 90) else 0
        vol_signal = 1 if self.volatility[0] > np.percentile(np.array(list(self.volatility)[1:]), 90) else 0

        signals:List[int] = [afd_signal, ord_signal, vol_signal]
        traded_direction:int = 1 if all(x==1 for x in signals) else -1

        # sort and divide to quantiles
        sorted_by_momentum:List[Symbol] = sorted([(symbol, self.data[symbol].get_monthly_return()) for symbol, value in fd_by_symbol.items()], key=lambda x: x[1])
        quantile:int = len(sorted_by_momentum) // self.quantile
        top_portfolio:List[Symbol] = [i[0] for i in sorted_by_momentum][-quantile:]
        bottom_portfolio:List[Symbol] = [i[0] for i in sorted_by_momentum][:quantile]

        # trade execution
        invested:List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
        for symbol in invested:
            if symbol not in top_portfolio + bottom_portfolio:
                self.Liquidate(symbol)

        for i, portfolio in enumerate([top_portfolio, bottom_portfolio]):
            for symbol in portfolio:
                if symbol in data and data[symbol]:
                    self.SetHoldings(symbol, ((-1) ** i) * traded_direction / len(top_portfolio))

Leave a Reply

Discover more from Quant Buffet

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

Continue reading