Quant Buffet放轻松,别过度思虑

使用五因子法玛-弗伦奇阿尔法的美国行业轮动策略

登录后收藏

学术论文

US Sector Rotation with Five-Factor Fama-French Alphas

作者US Sector Rotation with Five-Factor Fama-French Alphas [点击查看论文]

机构
  • University of Greenwich
  • ?University of Greenwich - Business School
  • DKAalborg University
  • ?Aalborg University Business School
  • City, University of London
  • ?City University London - The Business School

策略概要

该策略针对10个Fama-French美国行业投资组合,可通过行业ETF复制:消费非耐用品、消费耐用品、制造业、能源、高科技、电信、商店、医疗保健、公用事业和其他。使用36个月的滚动窗口,估算FF5阿尔法(基于市场风险、规模、价值、盈利能力和投资因子)。选择阿尔法为正的投资组合在t+1个月做多。使用更新的阿尔法每月重新平衡头寸。投资组合等权重,提供了一种由基于因子的阿尔法预测驱动的基于行业的系统性投资方法。

II. 策略合理性

证据表明,Fama-French三因子模型(FF3)无法完全解释股票回报的横截面,尤其是在盈利能力和投资方面。虽然FF3调整了资本资产定价模型中的超额收益趋势,但其在解释横截面回报变化方面的局限性导致了五因子模型(FF5)的开发。FF5模型包括盈利能力和投资因子,提供了比FF3更好的拟合度。研究表明,FF5模型显著提高了行业轮动策略的回报,为理解和预测不同股票类别的预期回报提供了一个更全面的框架。

回测表现

波动率15.83%
夏普比率0.7
索提诺比率0.23
胜率82%

完整 Python 代码

from AlgorithmImports import *
from data_tools import QuantpediaFamaFrench, CustomFeeModel, FamaFrenchData, \
MultipleLinearRegression, SymbolData
class USSectorRotationFiveFactorFamaFrenchAlphas(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.leverage:int = 10
self.period:int = 37
self.total_factor_num:int = 5
self.max_missing_days:int = 5
self.min_prices:int = 15
self.min_daily_perfs:int = 15
self.recent_month:int = -1
self.data:dict[Symbol, SymbolData] = {}
self.long:list[Symbol] = []
self.fama_french:Symbol = self.AddData(QuantpediaFamaFrench, 'fama_french_5_factor', Resolution.Daily).Symbol
self.ff_factor_names:list[str] = ['market', 'size', 'value', 'profitability', 'investment']
self.ff_data:dict[str, FamaFrenchData] = { ff_factor_name: FamaFrenchData(self.period) for ff_factor_name in self.ff_factor_names }

# self.symbols = []
self.tickers = [
    "VNQ",  # Vanguard Real Estate Index Fund
    "XLK",  # Technology Select Sector SPDR Fund
    "XLE",  # Energy Select Sector SPDR Fund
    "XLV",  # Health Care Select Sector SPDR Fund
    "XLF",  # Financial Select Sector SPDR Fund
    "XLI",  # Industrials Select Sector SPDR Fund
    "XLB",  # Materials Select Sector SPDR Fund
    "XLY",  # Consumer Discretionary Select Sector SPDR Fund
    "XLP",  # Consumer Staples Select Sector SPDR Fund
    "XLU"   # Utilities Select Sector SPDR Fund
]
for symbol in self.tickers:
    data:Security = self.AddEquity(symbol, Resolution.Daily)
    data.SetFeeModel(CustomFeeModel())
    data.SetLeverage(self.leverage)
    
    self.data[data.Symbol] = SymbolData(self.period)

self.settings.minimum_order_margin_portfolio_percentage = 0.
self.settings.daily_precise_end_time = False

def OnData(self, data):
curr_date:datetime.date = self.Time.date()
# store FF performance values
if self.fama_french in data and data[self.fama_french]:
    for ff_factor_name, factor_data in self.ff_data.items():
        ff_factor_value:float = float(data[self.fama_french].GetProperty(ff_factor_name))
        factor_data.update_daily_perfs(ff_factor_value / 100.)
# store etf prices
for symbol, symbol_data in self.data.items():
    if symbol in data and data[symbol]:
        price:float = data[symbol].Value
        symbol_data.update_prices(price)

# monthly rebalance
if self.Time.month == self.recent_month:
    return
self.recent_month = self.Time.month
if self.securities[self.fama_french].get_last_data() and self.time.date() > QuantpediaFamaFrench.get_last_update_date()[self.fama_french]:
    self.liquidate()
    return
# check FF data presence
for ff_factor_name, ff_factor_data in self.ff_data.items():
        
    if ff_factor_data.daily_perfs_ready(self.min_daily_perfs):
        ff_factor_data.update_monthly_perfs()
    ff_factor_data.reset_daily_pefs()
# regression model
regression_x:list[list[float]] = [factor_data.get_regression_data(self.period - 1) for _, factor_data in self.ff_data.items() \
    if factor_data.monthly_perfs_ready()]
for symbol, symbol_data in self.data.items():
    if symbol_data.prices_ready(self.min_prices):
        symbol_data.update_monthly_perfs()
    if symbol_data.monthly_perfs_ready() and len(regression_x) == self.total_factor_num:
        monthly_perfs:list[float] = symbol_data.get_regression_data(self.period - 1)
        regression_model = MultipleLinearRegression(regression_x, monthly_perfs)
        ff5_alpha:float = regression_model.params[0]
        symbol_data.update_ff5_alpha(ff5_alpha)
        if symbol_data.ff5_alpha_ready() and symbol_data.are_all_ff5_alpha_positive():
            self.long.append(symbol)
    else:
        # make sure ff5_alpha are consecutive
        symbol_data.reset_ff5_alpha()
    symbol_data.reset_daily_pefs()

# trade execution
long_length:int = len(self.long)
stocks_invested:list[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in stocks_invested:
    if symbol not in self.long:
        self.Liquidate(symbol)
for symbol in self.long:
    if symbol in data and data[symbol]:
        self.SetHoldings(symbol, 1 / long_length)

self.long.clear()