“该策略使用15种发达国家货币,应用均值-方差优化,预期回报来自远期贴现,协方差矩阵根据过去的汇率波动估算。投资组合每月重新平衡。”

I. 策略概要

投资范围包括15种发达国家货币。为了确定投资组合权重,投资者使用均值-方差优化,预期回报等于远期贴现,协方差矩阵基于六个月内平方汇率增长的指数加权移动平均值。协方差矩阵被特征分解为特征向量(W矩阵)和特征值(lambda矩阵),并移除解释方差小于1%的主成分。修改后的协方差矩阵用于计算投资组合权重,并根据投资者的相对风险厌恶进行调整。投资组合每月重新平衡。

II. 策略合理性

该策略通过利用远期贴现(近似等于预期回报)在外汇市场中使用均值-方差优化,有助于减少估计误差。本文还解决了协方差矩阵估计误差的问题,并建议使用主成分分析(PCA)来更好地捕捉外汇市场风险。PCA移除了低方差成分,提高了协方差矩阵的稳健性,并有助于避免接近套利的机会。这种方法提高了样本外夏普比率,并将偏度转为正值。本文还强调了市场择时在提高业绩方面的重要性,表明风险限制(如保持恒定的名义价值)对回报产生负面影响。与其他学术策略相比,该方法在考虑交易成本时表现更优,并且在包括NBER衰退和欧元推出后的各个时期内都保持盈利。此外,在1983年至2016年的较短样本期内,它仍然表现良好。

III. 来源论文

Market Timing and Predictability in FX Markets [点击查看论文]

<摘要>

我们研究了外汇市场中市场择时的经济价值,即利用关于条件夏普比率的信息来调整条件均值-方差有效货币投资组合的名义价值。当条件风险回报权衡更有利(不利)时,我们的策略交易更积极(消极)。这导致样本外无条件夏普比率、偏度和每1%预期超额回报的最大回撤显著改善。该策略的市场择时预测外汇市场的回报、波动性和偏度。流行的货币定价因子无法解释该策略的高平均超额回报。我们的研究结果表明,在构建货币交易策略时,施加杠杆或风险(即条件波动率)限制或其他较差的市场择时政策是代价高昂的。

IV. 回测表现

年化回报7.44%
波动率8.12%
β值0.065
夏普比率0.92
索提诺比率-0.597
最大回撤16.87%
胜率66%

V. 完整的 Python 代码

from AlgorithmImports import *
import data_tools
from scipy.optimize import minimize
from enum import Enum
# endregion
class TradedUniverse(Enum):
    FX = 1
    FX_FUTURES = 2
class MeanVarianceMarketTimingInTheFXMarket(QCAlgorithm):
    def initialize(self) -> None:
        self.set_start_date(2000, 1, 1)
        self.set_cash(1_000_000)
        self._us_ir: Symbol = self.AddData(data_tools.InterestRate3M, 'IR3TIB01USM156N', Resolution.Daily).Symbol
        self._traded_universe: TradedUniverse = TradedUniverse.FX_FUTURES
        period: int = 6
        self._min_weight: float = .01
        self._EWMA_lambda: float = .95
        self._data: Dict[Symbol, data_tools.SymbolData] = {}
        # Cash rate source: https://fred.stlouisfed.org/series/IR3TIB01USM156N
        if self._traded_universe == TradedUniverse.FX:
            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
            }
        elif self._traded_universe == TradedUniverse.FX_FUTURES:
            symbols: Dict[str, str] = {
                "CME_AD1" : "IR3TIB01AUM156N",   # Australian Dollar Futures, Continuous Contract #1
                "CME_BP1" : "LIOR3MUKM",         # British Pound Futures, Continuous Contract #1
                "CME_CD1" : "IR3TIB01CAM156N",   # Canadian Dollar Futures, Continuous Contract #1
                "CME_EC1" : "IR3TIB01EZM156N",   # Euro FX Futures, Continuous Contract #1
                "CME_JY1" : "IR3TIB01JPM156N",   # Japanese Yen Futures, Continuous Contract #1
                "CME_MP1" : "IR3TIB01MXM156N",   # Mexican Peso Futures, Continuous Contract #1
                "CME_NE1" : "IR3TIB01NZM156N",   # New Zealand Dollar Futures, Continuous Contract #1
                "CME_SF1" : "IR3TIB01CHM156N"    # Swiss Franc Futures, Continuous Contract #1
            }
        # data subscription
        for symbol, rate_symbol in symbols.items():
            if self._traded_universe == TradedUniverse.FX:
                data: Security = self.add_forex(symbol, Resolution.MINUTE, Market.OANDA)
            elif self._traded_universe == TradedUniverse.FX_FUTURES:
                data: Security = self.add_data(data_tools.QuantpediaFutures, symbol, Resolution.DAILY)
            data.set_fee_model(data_tools.CustomFeeModel())
            ir_symbol: Symbol = self.add_data(data_tools.InterestRate3M, rate_symbol, Resolution.DAILY).symbol
            self._data[data.symbol] = data_tools.SymbolData(period, ir_symbol)
        self.settings.daily_precise_end_time = False
        self.settings.minimum_order_margin_portfolio_percentage = 0.
        self._recent_month: int = -1
    def on_data(self, slice: Slice) -> None:
        if slice.contains_key(self._us_ir) and slice[self._us_ir]:
            for symbol, symbol_data in self._data.items():
                if slice.contains_key(symbol_data._ir_symbol) and slice[symbol_data._ir_symbol]:
                    symbol_data.update_values(
                        slice[symbol_data._ir_symbol].value - slice[self._us_ir].value, slice[symbol_data._ir_symbol].value
                    )
        if self._traded_universe == TradedUniverse.FX:
            if not self.securities[list(self._data.keys())[0]].exchange.hours.is_open(self.time, extended_market_hours=False):
                return
        # monthly rebalance
        if self._recent_month == self.time.month:
            return
        self._recent_month = self.time.month
        last_update_date: Dict[str, datetime.date] = data_tools.QuantpediaFutures.get_last_update_date()
        
        EWMA: Dict[Symbol, float] = {
            symbol: data_tools.EWMA_Volatility(symbol_data.get_rate_diff(), self._EWMA_lambda) 
            for symbol, symbol_data in self._data.items() 
            if symbol_data.is_ready()
            and (symbol.value in last_update_date and last_update_date[symbol.value] > self.time.date() if self._traded_universe == TradedUniverse.FX_FUTURES else True)
        }
        
        if len(EWMA) == 0:
            self.log('Not enough data for further calculation.')
            return
        
        expected_ret_df: dataframe = pd.concat(
            [symbol_data.get_expected_returns() for _, symbol_data in self._data.items() if symbol_data.is_ready()], axis=1
        )
        port_opt = data_tools.PortfolioOptimization(expected_ret_df, 0, len(expected_ret_df.columns), np.mean(list(EWMA.values())))
        w: np.ndarray = port_opt.opt_portfolio()
        targets: List[PortfolioTarget] = []
        for i, symbol in enumerate(EWMA):
            if w[i] > self._min_weight:
                if slice.contains_key(symbol) and slice[symbol]:
                    targets.append(PortfolioTarget(symbol, w[i]))
        
        self.set_holdings(targets, True)

发表评论

了解 Quant Buffet 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读