“该策略通过6个月的动量选择前5只ETF,利用最小方差优化权重,目标为8%的波动率,并使用历史波动率和相关性每周重新平衡。”

I. 策略概要

该策略投资于代表主要资产类别的10只ETF(美国、欧洲、日本、新兴市场股票,美国和国际房地产投资信托基金、美国国债、商品和黄金)。每周,根据6个月的动量选择排名前5的资产类别。权重使用60天历史波动率和相关性进行最小方差优化计算。根据这些权重预测整体波动率,并调整至目标8%的波动率。该组合每周重新平衡,以维持最优权重和波动率目标,结合基于动量的选择和基于风险的配置,形成一种多元化、适应性的投资策略。

II. 策略合理性

该策略利用了动量效应,这是最可靠和最有文献记录的异常之一。通过使用最小方差优化来实现有吸引力的回报/风险特征,并通过使用短期数据(因为相关性和波动率在短期时间间隔内更稳定)来改进这一计算方法。

III. 来源论文

自适应资产配置:入门 [点击查看论文]

<摘要>

该论文探讨了与战略资产配置(SAA)相关的现代投资组合理论传统应用中的缺陷。基于长期观察的平均值对投资组合优化的参数估计被证明不如基于较短时间框架观察的替代估计方法。接着,提出了一种自适应资产配置(Adaptive Asset Allocation)投资组合构建框架,能够以一种连贯的方式整合投资组合参数,从而在测试期内显著提高相较于战略资产配置(SAA)的表现。

IV. 回测表现

年化回报15.1%
波动率9.4%
β值0.611
夏普比率1.61
索提诺比率0.228
最大回撤-8.8%
胜率54%

V. 完整的 Python 代码

from AlgorithmImports import *
# The investment universe consists of 10 ETFs which are proxy for the main asset classes (US Stocks, European Stocks, Japanese Stocks, EM Stocks, US REITs, 
# International REITs, US Intermediate Treasuries, US Long-term Treasuries, Commodities, and Gold). Each week, the asset classes are sorted based on their 
# 6-month momentum. Only the top 5 assets are then used in the next step, when the investor uses the minimum variance calculation to compute the weights 
# for each asset class for the next week. 60-day historical volatilities and correlations are used in the minimum variance computation. The overall portfolio
# volatility prediction is then estimated (based on the weights from the min. variance algorithm and on historical volatilities and correlations) 
# and the portfolio is rescaled to target 8% volatility. These steps are performed every week and the portfolio is rebalanced accordingly.
import pandas as pd
import numpy as np
from scipy.optimize import minimize
class AdaptiveAssetAllocation(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2008, 1, 1)
        self.SetCash(100000)
        self.symbols = ['SPY', 'VGK', 'EWJ', 'EEM', 'VNQ', 'RWX', 'IEI', 'IEF', 'DBC', 'GLD']
        self.period = 6 * 21
        self.data = {}
        
        for symbol in self.symbols:
            self.AddEquity(symbol, Resolution.Daily)
            self.data[symbol] = SymbolData(self.period)
        self.Schedule.On(self.DateRules.WeekStart(self.symbols[0]), self.TimeRules.AfterMarketOpen(self.symbols[0]), self.Rebalance)
    def OnData(self, data):
        for symbol in self.data:
            symbol_obj = self.Symbol(symbol)
            if symbol_obj in data and data[symbol_obj]:
                self.data[symbol].update(data[symbol_obj].Value)
        
    def Rebalance(self):
        self.Liquidate()
        
        ret_data = { x : self.data[x].performance() for x in self.symbols if self.data[x].is_ready()}
        # Performance sorting.
        if len(ret_data) == 0: return
        sorted_by_ret = sorted(ret_data.items(), key = lambda x: x[1], reverse = True)
        top_symbols = [x[0] for x in sorted_by_ret[:5]]
        
        # Optimalization
        data = {}
        for symbol in top_symbols:
            closes = [x for x in self.data[symbol].price]
            data[symbol] = closes[:60]
            
        df_price = pd.dataframe(data, columns=data.keys()) 
        daily_return = (df_price / df_price.shift(1)).dropna()
        a = PortfolioOptimization(daily_return, 0, len(data))
        opt_weight = a.opt_portfolio()
        
        for i in range(len(data)):
            # Weight is higher than minumum QC supported weight.
            if opt_weight[i] > 0.001:
                self.SetHoldings(df_price.columns[i], opt_weight[i])
        
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.08
    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
        # method 3: minimize variance with target volatility
        return (1 / self.annual_port_vol(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
class SymbolData():
    def __init__(self, period):
        self.price = RollingWindow[float](period)
        
    def update(self, value):
        self.price.Add(value)
    
    def is_ready(self) -> bool:
        return self.price.IsReady
        
    def performance(self, values_to_skip = 0) -> float:
        closes = [x for x in self.price][values_to_skip:]
        return (closes[0] / closes[-1] - 1)

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读