“该策略投资于Alpha Architect的因子投资数据库中的因子,涵盖价值、质量、动量、规模和波动性,基于美国前1500支股票。首先构建快速信号(过去一个月回报)和慢速信号(过去十二个月回报),并根据其绝对值排名。信号的权重由排名和符号决定,动态混合策略的最终权重为快速信号的四分之三加慢速信号的四分之一。通过移动平均线确定市场和因子投资组合的权重,每月根据回报重新平衡。”
资产类别:ETF、股票 | 地区:全球 | 频率:每月 | 市场:股票 | 关键词:动量、市场
策略概述
投资范围包括 Alpha Architect 的因子投资数据库中的因子(包括价值、质量、动量、规模和波动性等主要投资风格因子),基于美国前 1500 支股票。首先构建每个因子的快速信号和慢速信号。快速信号是过去一个月的回报,慢速信号是过去十二个月的回报。对于每种类型的信号,根据其绝对值在横截面上进行排名。个别慢速或快速信号的权重等于相应的排名除以所有排名之和,然后乘以信号的符号(文中的方程3和方程4)。对于动态混合策略(智能因子策略),每个因子的最终权重为其快速信号权重的四分之三加上慢速信号权重的四分之一(方程12)。接下来,将美国前 1500 支股票作为市场投资组合。组合智能因子和市场策略使用过去回报的移动平均线来确定市场和因子投资组合的权重。组合策略回顾过去十二个月的回报和十二个回报的移动平均线。假设用于主动投资(因子动量)的移动平均值大于用于市场投资组合的移动平均值,则主动投资得一分。否则,市场投资组合得一分。因此,每个月,因子动量和市场投资组合的权重由“获胜”(失败)移动平均值的数量确定(方程13和方程14)。策略每月重新平衡。
策略合理性
首先,因子策略的功能性已经被大量学术研究证明。同样可以说的是,因子动量也是如此,因为时间序列和横截面动量策略都经过了深入研究,并被证明是有效的。因子动量还解决了因错误的投资组合排序而导致因子表现不佳的问题(例如,当增长优于价值或大型股票优于小型股票时)。
因子混合似乎是重要的,因为慢速信号往往对趋势变化反应不足,而快速信号则经常发出错误警报。因此,因子的权重应根据信号之间的相互作用进行调整。最后,基于信号强度的动态权重也是一种被广泛采用且在因子领域被证明有效的方法。
尽管主动因子策略在很大程度上优于因子或信号的简单等权重策略,但它仍然会大幅落后于市场。然而,主动因子策略和市场之间存在负相关性。通过强大的非参数检验,这种相关性具有统计学上的显著性,这一结果表明这两个投资组合可以结合起来,以实现两种方法的最佳效果。回测结果证实了这一理论,因为使用移动平均线的综合策略具有最高的回报、最低的波动率或回撤,而且回报分布更加有利。
论文来源
The active vs passive: smart factors, market portfolio or both? [点击浏览原文]
- Matus Padysak, 斯洛伐克布拉迪斯拉发夸美纽斯大学 – 数学、物理与信息学学院
<摘要>
尽管 passvie 和 active 投资存在争议,甚至有关于被市场超过的 active 基金数量的博客,但历史告诉我们,active 或 passive 投资的超额收益是循环的。作为 active 投资的代理,本文研究了因子策略及其智能配置,利用快速或慢速时间序列动量信号、基于信号强度的相对权重,甚至混合信号。虽然利用这些智能方法可以显著提高绩效,但在美国和 EAFE 样本中,因子仍然被市场击败。然而, passvie 方法并未表现出优越性。因子策略和市场之间存在显著负相关,并且令人印象深刻地相互补充。在整个样本期内,Smart Factors 和市场组合的综合表现远远优于因子和市场。通过综合方法,市场的持续下跌至少可以得到缓解,或者得益于因子。


回测表现
| 年化收益率 | 11.9% |
| 波动率 | 10.46% |
| Beta | 0.007 |
| 夏普比率 | 0.384 |
| 索提诺比率 | N/A |
| 最大回撤 | 12.2% |
| 胜率 | 58% |
Python代码及解释
完整python代码
from AlgoLib import *
import numpy as np
#endregion
class CombiningSmartFactorsMomentumandMarketPortfolio(XXX):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.symbols = {
'momentum' : 'US_EQUAL_DECILE_1500_12_2m_L_S',
'value' : 'US_EQUAL_DECILE_1500_B_M_L_S',
'quality' : 'US_EQUAL_DECILE_1500_ROA_L_S',
'size' : 'US_EQUAL_DECILE_1500_Size_L_S',
'volatility' : 'US_EQUAL_DECILE_1500_Volatility_L_S',
}
# monthly price data
self.data = {}
self.long_period = 13
self.short_period = 2
self.max_missing_days:int = 5
self.monthly_returns = {}
self.monthly_returns_period = 12
for symbol, equity_symbol in self.symbols.items():
data = self.AddData(USEquity, equity_symbol, Resolution.Daily)
data.SetLeverage(10)
data.SetFeeModel(CustomFeeModel())
self.data[symbol] = RollingWindow[float](self.long_period)
self.market = self.AddEquity("IWM", Resolution.Daily).Symbol
self.data[self.market] = RollingWindow[float](self.short_period)
self.monthly_returns['smart_factors'] = RollingWindow[float](self.monthly_returns_period)
self.monthly_returns['market'] = RollingWindow[float](self.monthly_returns_period)
self.recent_month:int = -1
def OnSecuritiesChanged(self, changes):
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(5)
def OnData(self, data):
# store factor monthly prices
for symbol, equity_symbol in self.symbols.items():
if equity_symbol in data and data[equity_symbol]:
price = data[equity_symbol].Value
self.data[symbol].Add(price)
# store market prices
if self.market in data and data[self.market]:
market_price = data[self.market].Value
self.data[self.market].Add(market_price)
if self.recent_month == self.Time.month:
return
self.recent_month = self.Time.month
slow_momentum = {}
fast_momentum = {}
# calculate both momentum values
for symbol, equity_symbol in self.symbols.items():
if self.Securities[equity_symbol].GetLastData() and (self.Time.date() - self.Securities[equity_symbol].GetLastData().Time.date()).days <= self.max_missing_days:
if self.data[symbol].IsReady:
slow_momentum[symbol] = self.data[symbol][0] / self.data[symbol][self.long_period-1] - 1
fast_momentum[symbol] = self.data[symbol][0] / self.data[symbol][1] - 1
total_weight = {}
if len(fast_momentum) != 0:
# momentum ranking
# weights
rank_sum = sum([x for x in range(1, len(slow_momentum)+1)])
sorted_by_slow_momentum = sorted(slow_momentum.items(), key = lambda x: abs(x[1]), reverse = False)
slow_weight = {}
for i, (symbol, momentum) in enumerate(sorted_by_slow_momentum):
rank = i+1
slow_weight[symbol] = (rank / rank_sum) * np.sign(momentum)
sorted_by_fast_momentum = sorted(fast_momentum.items(), key = lambda x: abs(x[1]), reverse = False)
fast_weight = {}
for i, (symbol, momentum) in enumerate(sorted_by_fast_momentum):
rank = i+1
fast_weight[symbol] = (rank / rank_sum) * np.sign(momentum)
# total weight
for symbol, equity_symbol in self.symbols.items():
if symbol in slow_momentum and symbol in fast_momentum:
s_weight = slow_weight[symbol]
f_weight = fast_weight[symbol]
total_weight[symbol] = 0.75*f_weight + 0.25*s_weight
# retrun calculation for market and smart factors
if self.data[self.market].IsReady:
market_return = self.data[self.market][0] / self.data[self.market][1] - 1
self.monthly_returns['market'].Add(market_return)
# smart factor return calculation
smart_factors_return = 0
for symbol, momentum_1M in fast_momentum.items():
if symbol in total_weight:
w = total_weight[symbol]
symbol_ret = w*momentum_1M
smart_factors_return += symbol_ret
if smart_factors_return != 0:
self.monthly_returns['smart_factors'].Add(smart_factors_return)
score = {}
traded_weight = {}
# calculate 12 SMA's
if self.monthly_returns['smart_factors'].IsReady and self.monthly_returns['market'].IsReady:
score['smart_factors'] = 0
score['market'] = 0
for sma_period in range(1, 13):
factor_returns = [x for x in self.monthly_returns['smart_factors']][:sma_period]
market_returns = [x for x in self.monthly_returns['market']][:sma_period]
factor_mean_return = np.mean(factor_returns)
market_mean_return = np.mean(market_returns)
if factor_mean_return > market_mean_return:
score['smart_factors'] += 1
else:
score['market'] += 1
total_score = score['market'] + score['smart_factors']
if total_score != 0:
traded_weight['market'] = score['market'] / total_score
traded_weight['smart_factors'] = score['smart_factors'] / total_score
# order execution
# market
self.SetHoldings(self.market, traded_weight['market'])
# smart factors
for symbol, equity_symbol in self.symbols.items():
if symbol in total_weight:
w = total_weight[symbol]
self.SetHoldings(equity_symbol, traded_weight['smart_factors'] * w)
class USEquity(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource("data.quantpedia.com/backtesting_data/equity/us_ew_decile/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
# File example.
# date;equity
# 1992-01-31;0.98
def Reader(self, config, line, date, isLiveMode):
data = USEquity()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(';')
# Prevent lookahead bias.
data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
data.Value = float(split[1])
return data
# Custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))
