该策略根据美联储政策调整投资组合:在紧缩阶段增加商品配置,在扩张阶段持有股票和债券的均值-方差高效组合,灵活适应经济条件的变化。

I. 策略概述

该策略通过追踪GSCI指数的ETF获取商品敞口。根据美联储的利率政策,将市场分为扩张阶段(降息)和紧缩阶段(加息)。在扩张阶段,投资者持有包括美国股票、EAFE(欧洲、澳大利亚和远东地区)股票、国债和公司债券的均值-方差高效组合,这些资产通过ETF追踪。在紧缩阶段,增加商品配置,并重新计算均值-方差高效组合的权重。风险和收益指标专门针对紧缩时期设计,以反映投资组合对美联储货币政策收紧的调整能力。该策略通过动态资产配置适应当前经济条件。

II. 策略合理性

学术研究表明,商品期货是有效的通胀对冲工具,而在通胀上升时期,股票和债券通常表现不佳。美联储提高利率表明其在努力抑制通胀,这为投资者提供了增加商品期货配置的信号。在这种情况下,商品期货可以通过提高投资组合的风险调整回报率,帮助投资者应对市场变化。

III. 论文来源

Tactical Asset Allocation and Commodity Futures [点击浏览原文]

<摘要>

本文研究了在传统投资组合(包括美国股票、外国股票、公司债券和国库券)中添加管理型和非管理型商品期货的分散化收益。在1973年至1999年期间,研究发现商品期货显著提升了投资组合的表现,其中管理型期货的收益最大。这些收益几乎完全出现在美联储采取紧缩货币政策的时期。研究表明,金属和农产品期货合约为投资者提供了最显著的分散化收益。总体而言,研究结果表明,投资者应根据货币条件评估商品期货在投资组合中的最优配置,并判断是否应在某种特定合约上建立多头或空头头寸。

IV. 回测表现

年化收益率20.46%
波动率13.86%
Beta0.453
夏普比率1.48
索提诺比率N/A
最大回撤N/A
胜率51%

V. 完整python代码

from math import isnan
from AlgorithmImports import *
import pandas as pd
import numpy as np
from scipy.optimize import minimize
class CommoditiesTimingbasedonaMonetaryConditions(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2004, 1, 1)
        self.SetCash(100000)
        
        self.data = {}  # daily closes
        period = 12 * 21
        self.SetWarmUp(period)
        
        self.symbols = ["SPY", "EFA", "IEF", "LQD"]
        self.commodities = ["DBC"]
        for symbol in self.symbols + self.commodities:
            self.AddEquity(symbol, Resolution.Daily)
            self.data[symbol] = RollingWindow[float](period)
        # changes in FED policy from restrictive to expansive
        dates_str = ["24.08.1999", "03.01.2001", "30.06.2004", "18.09.2007", "14.12.2016", "31.7.2019"]
        self.dates = [datetime.strptime(x, "%d.%m.%Y").date() for x in dates_str] # datetime type
        
        # import quandl federal rate data
        self.target_rate = self.AddData(QuandlValue, 'FRED/DFEDTARU', Resolution.Daily).Symbol
        self.restrictive_flag = True    # from start of the algorithm
        self.last_target_rate = None
        self.external_restrictive_flag = None
        self.recent_month = -1
    def OnData(self, data):
        if self.target_rate in data and data[self.target_rate]:
            curr_target_rate = data[self.target_rate].Value
            
            restrictive_flag = None
            
            # switch to external data source, when self.dates ends
            if self.Time.date() > self.dates[-1]:
                if self.last_target_rate:
                    if curr_target_rate > self.last_target_rate:
                        restrictive_flag = True
                    elif curr_target_rate < self.last_target_rate:
                        restrictive_flag = False
            
            if restrictive_flag is not None:
                self.external_restrictive_flag = restrictive_flag
                    
            self.last_target_rate = curr_target_rate
        
        # store daily price data
        for symbol in self.symbols + self.commodities:
            if symbol in data and data[symbol]:
                price = data[symbol].Value
                self.data[symbol].Add(price)
        
        if self.recent_month == self.Time.month:
            return
        self.recent_month = self.Time.month
        
        self.Liquidate()
        # if external data is present, trade out of it
        if self.external_restrictive_flag is not None:
            restrictive_flag = self.external_restrictive_flag
            
            # stop backtest once quandl target rate data stops
            if self.Securities[self.target_rate].GetLastData() and (self.Time.date() - self.Securities[self.target_rate].GetLastData().Time.date()).days > 5:
                self.Liquidate()
                return
        else:
            restrictive_flag = self.restrictive_flag
        symbols = [x for x in self.symbols]
        
        if restrictive_flag:
            symbols += self.commodities
        # construct dataframe
        data = {}
        for symbol in symbols:
            if self.data[symbol].IsReady:
                data[symbol] = [x for x in self.data[symbol]][::-1]
                
        if len(data) != 0:
            df_price = pd.dataframe(data,columns=data.keys()) 
            daily_return = (df_price / df_price.shift(1) - 1).dropna()
            a = PortfolioOptimization(daily_return, 0, len(data))
            opt_weight = a.opt_portfolio()
            
            if isnan(sum(opt_weight)): return
            
            for i in range(len(data)):
                if opt_weight[i] >= 0.001:
                    self.SetHoldings(df_price.columns[i], opt_weight[i])
        else:
            if self.Portfolio.Invested:
                self.Liquidate()
            
class PortfolioOptimization(object):
    def __init__(self, df_return, risk_free_rate, num_assets):
        self.daily_return = df_return
        self.risk_free_rate = risk_free_rate
        self.n = num_assets # numbers of risk assets in portfolio
        self.target_vol = 0.05
    def annual_port_return(self, weights):
        # calculate the annual return of portfolio
        return np.sum(self.daily_return.mean() * weights) * 252
    def annual_port_vol(self, weights):
        # calculate the annual volatility of portfolio
        return np.sqrt(np.dot(weights.T, np.dot(self.daily_return.cov() * 252, weights)))
    def min_func(self, weights):
        # method 1: maximize sharp ratio
        return - self.annual_port_return(weights) / self.annual_port_vol(weights)
        
        # method 2: maximize the return with target volatility
        #return - self.annual_port_return(weights) / self.target_vol
    def opt_portfolio(self):
        # maximize the sharpe ratio to find the optimal weights
        cons = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
        bnds = tuple((0, 1) for x in range(2)) + tuple((0, 0.25) for x in range(self.n - 2))
        opt = minimize(self.min_func,                               # object function
                       np.array(self.n * [1. / self.n]),            # initial value
                       method='SLSQP',                              # optimization method
                       bounds=bnds,                                 # bounds for variables 
                       constraints=cons)                            # constraint conditions
                      
        opt_weights = opt['x']
 
        return opt_weights
# Quandl "value" data
class QuandlValue(PythonQuandl):
    def __init__(self):
        self.ValueColumnName = 'Value'




发表评论

了解 Quant Buffet 的更多信息

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

继续阅读