该策略投资于AMEX、NYSE和NASDAQ上市的美国股票,数据来源于Kenneth French网站。基于规模、价值、动量、投资和盈利能力五个因子创建投资组合,利用马科维茨模型构建多空有效投资组合,以最大化夏普比率。每月使用前60个月的数据进行样本外估计。

策略概述

投资范围包括所有在AMEX、NYSE和NASDAQ上市的美国股票。数据来自Kenneth French的网站。基于五个因子:规模、价值、动量、投资和盈利能力创建因子投资组合。使用马科维茨模型,构建一个多空有效投资组合,以最大化夏普比率。每月使用前60个月的数据进行样本外估计。

策略合理性

因子投资已被学术界充分研究,过去的研究显示因子投资的功能性得到了强有力的支持,并证明可以通过将异常现象结合起来提高投资组合的盈利能力。作者确认了这一点。

首先,作者基于标准工业分类(SIC)创建了十个行业投资组合:非耐用消费品、耐用消费品、制造业、能源、高科技、电信、商店、医疗保健、公用事业和其他行业,以及基于规模、价值、盈利能力、投资和动量的因子投资组合。实际上,这些因子本身不可直接投资,但投资者可以交易交易型开放式指数基金(ETF)或基于因子的共同基金。

其次,他们使用马科维茨模型确定了行业和因子投资组合的有效前沿。他们构建了一个市场组合,其收益是所有NYSE、Amex和Nasdaq上市的美国公司的收益的价值加权平均数,并将其与有效前沿进行比较。Basak(2002)提出的第一个测试基于“水平距离”,它衡量市场组合与相同收益的有效组合之间的波动率差距。Briére(2013)提出了“垂直距离”,测量市场组合的预期收益与其相同方差的有效组合之间的距离。最终,作者利用了垂直和水平距离。

随后,他们比较了基于行业和因子构建的投资组合的Jensen阿尔法和夏普比率,并考察了最大化夏普比率的有效投资组合、最小波动性投资组合和等权重投资组合。最后,他们进行了样本内和样本外测试,以增加测试的稳健性。正如前文提到的,结果显示,当没有卖空限制时,因子投资比行业投资表现更好。

论文来源

When it Rains, it Pours: Multifactor Asset Management in Good and Bad Times [点击浏览原文]

<摘要>

我们研究了美国股票市场上多因子投资组合的盈利能力。以被动的行业投资为基准,我们评估了因子资产管理策略在好时光和坏时光中的表现。在没有卖空限制的情况下,因子投资在各个方面都优于行业投资。对于仅做多的投资组合,我们的结果揭示了因子相关的风险溢价与行业多样化潜力之间的权衡。在经济良好时期,多因子投资往往比基准更具盈利性,而在经济不佳的时期,当多样化最为需要时,其吸引力较低。

回测表现

年化收益率56.27%
波动率17.05%
Beta0.223
夏普比率3.3
索提诺比率0.17
最大回撤N/A
胜率67%

完整python代码

from AlgorithmImports import *
from scipy.optimize import minimize
import data_tools
#endregion

class MeanVarianceFactorTiming(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2010, 1, 1)
        self.SetCash(100000)
        
        self.period:int = 60 * 21

        # warm up fama french values for idiosyncratic volatility
        self.SetWarmup(self.period, Resolution.Daily)

        self.data:dict = {}
        
        self.fama_french_symbol:Symbol = self.AddData(data_tools.QuantpediaFamaFrench, 'fama_french_5_factor', Resolution.Daily).Symbol
        self.ff_factor_names:list[str] = ['market', 'size', 'value', 'profitability', 'investment']

        # ff performance data
        self.fama_french_data:dict = { ff_factor_name : RollingWindow[float](self.period) for ff_factor_name in self.ff_factor_names }
        
        # ff traded symbols
        for factor_name in self.ff_factor_names:
            data:Security = self.AddData(data_tools.QuantpediaFamaFrenchEquity, f'fama_french_5_{factor_name}_eq', Resolution.Daily)
            data.SetLeverage(3)
            data.SetFeeModel(data_tools.CustomFeeModel())

        self.recent_month:int = -1

    def OnData(self, data):
        # update fama french values on daily basis
        if self.fama_french_symbol in data and data[self.fama_french_symbol]:
            for ff_factor_name in self.ff_factor_names:
                self.fama_french_data[ff_factor_name].Add(data[self.fama_french_symbol].GetProperty(ff_factor_name))
        
        if self.recent_month == self.Time.month:
            return
        self.recent_month = self.Time.month

        # optimization
        if all(x[1].IsReady for x in self.fama_french_data.items()):
            perf_df:pd.DataFrame = pd.DataFrame(columns=self.ff_factor_names)
            for ff_factor_name in self.ff_factor_names:
                perf_df[ff_factor_name] = np.array([x for x in self.fama_french_data[ff_factor_name]][::-1])

            opt, weights = self.optimization_method(perf_df)
            for ff_factor_symbol, w in weights.items():
                traded_symbol:str = f'fama_french_5_{ff_factor_symbol}_eq'
                if abs(w) > 0.001:
                    self.SetHoldings(traded_symbol, w)
                else:
                    self.Liquidate(traded_symbol)
        
    def optimization_method(self, returns:pd.DataFrame):
        '''Maximize sharpe ratio method'''
        # objective function
        fun = lambda weights: - np.sum(returns.mean() * weights) * 252 / np.sqrt(np.dot(weights.T, np.dot(returns.cov() * 252, weights)))

        # Constraint #1: The weights can be negative, which means investors can short a security.
        constraints = [{'type': 'eq', 'fun': lambda w: 1 - np.sum(w)}]

        size = returns.columns.size
        x0 = np.array(size * [1. / size])
        # bounds = tuple((self.minimum_weight, self.maximum_weight) for x in range(size))
        bounds = tuple((0, 1) for x in range(size))

        opt = minimize(fun,                         # Objective function
                       x0,                          # Initial guess
                       method='SLSQP',              # Optimization method:  Sequential Least SQuares Programming
                       bounds = bounds,             # Bounds for variables 
                       constraints = constraints)   # Constraints definition

        return opt, pd.Series(opt['x'], index = returns.columns)

Leave a Reply

Discover more from Quant Buffet

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

Continue reading