Quant Buffet放轻松,别过度思虑

在股票中在价值和动量之间切换

登录后收藏

学术论文

Application Of Tactical Style Allocation For Global Equity Portfolios

作者Hsieh

机构
  • ?Hodnett, van Rensburg
论文摘要

我们之前的研究表明,价值型和动量型这两种主要投资风格存在特定的时机。本研究在之前的基础上进行扩展,测试并评估了一个基于加权最小二乘法(WLS)技术的战术风格配置(TSA)模型,用于1994年至2008年的全球股票数据。本文构建了两种基于TSA风格的投资组合:一种是包含无风险代理(现金成分)、全球动量指数和全球价值指数的投资组合,另一种仅包含全球动量指数和全球价值指数的投资组合。基于TSA模型优化的投资组合在风险调整后,表现优于MSCI世界指数、全球价值指数和全球动量指数。风格配置组合中的现金成分在金融市场危机期间提供了必要的保护。我们的研究结果支持使用所提出的TSA模型,执行全球股票投资组合中价值股票与动量股票之间的主动风格轮换。

策略概要

该策略投资于全球股票,创建每月的价值和动量组合。价值组合包括收益与价格(E/P)比率最高的股票,而动量组合则包括过去6至12个月回报最高的股票。整体组合结合了价值、动量和现金(投资于国库券)。通过滚动36个月的加权最小二乘法(WLS)回归,投资者计算三种组件的最佳权重,以最大化夏普比率。将这些优化后的配置应用于下一个月,并每月重新平衡组合,以便与更新的权重保持一致,从而确保组合能动态适应市场变化。

策略合理性

研究表明,全球股市中的价值效应和动量效应受到投资者情绪的影响,而投资者情绪会随经济周期变化。在经济高峰期,过于乐观的投资者偏好动量股票,即那些近期表现强劲的股票,而忽视表现较差的价值股票,从而导致动量股票的超额回报。然而,在随后的市场调整中,这一趋势会发生逆转,价值股票开始反弹。这种情绪、经济状况与股票表现之间的关系表明,投资者可以开发时机策略,以利用动量股票和价值股票在经济周期不同阶段的交替超额表现。

回测表现

波动率11.4%
夏普比率0.99
索提诺比率0.432
胜率55%

完整 Python 代码

from math import isnan
import pandas as pd
import numpy as np
from scipy.optimize import minimize
from AlgorithmImports import *
class SwitchingbetweenValueMomentum(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
self.data:Dict[Symbol, SymbolData] = {}
self.period:int = 36 * 21
self.leverage:int = 5
self.quantile:int = 10
self.symbol:Symbol = self.AddEquity('IEF', Resolution.Daily).Symbol
self.data[self.symbol] = SymbolData(self, self.symbol, self.period) # T-note prices

self.portfolio_symbols:Set[Symbol] = set() # Selected sorted symbols
self.fundamental_count:int = 200
self.fundamental_sorting_key = lambda x: x.DollarVolume
self.selection_flag:bool = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)
self.settings.daily_precise_end_time = False
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
    security.SetFeeModel(CustomFeeModel())
    security.SetLeverage(self.leverage)
    
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
# Update the rolling window every day.
for stock in fundamental:
    symbol:Symbol = stock.Symbol
    
    # Store monthly price.
    if symbol in self.data:
        self.data[symbol].update(stock.AdjustedPrice)
if not self.selection_flag:
    return Universe.Unchanged
selected:List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and \
    not isnan(x.ValuationRatios.PERatio) and x.ValuationRatios.PERatio != 0
]
if len(selected) > self.fundamental_count:
    selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
ep:Dict[Symbol, float] = {}
performance:Dict[Symbol, float] = {}
# Warmup price rolling windows.
for stock in selected:
    symbol:Symbol = stock.Symbol
    if symbol not in self.data:
        self.data[symbol] = SymbolData(self, symbol, self.period)
    
    if self.data[symbol].is_ready():
        ep[symbol] = (1 / stock.ValuationRatios.PERatio)
        performance[symbol] = self.data[symbol].performance()

if len(ep) >= self.quantile:
    # Sorting by return and EP.
    sorted_by_ret:List = sorted(performance.items(), key = lambda x: x[1], reverse = True)
    quantile:int = int(len(sorted_by_ret) / self.quantile)
    high_by_ret:List[Symbol] = [x[0] for x in sorted_by_ret[:quantile]]
    
    sorted_by_ep:List = sorted(ep.items(), key = lambda x: x[1], reverse = True)
    quantile = int(len(sorted_by_ep) / self.quantile)
    high_by_ep:List[Symbol] = [x[0] for x in sorted_by_ep[:quantile]]
    
    self.portfolio_symbols = set(high_by_ret + high_by_ep)   # portfolios with no duplication

return list(self.portfolio_symbols)
def OnData(self, data: Slice) -> None:
if not self.selection_flag:
    return
self.selection_flag = False

self.Liquidate()

if len(self.portfolio_symbols) == 0: return

# Optimalization.
data:Dict = { symbol : [x for x in self.data[symbol].Price] for symbol in self.portfolio_symbols if symbol in data and data[symbol]}
data[self.symbol] = [x for x in self.data[self.symbol].Price]
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()

if isnan(sum(opt_weight)): return
for i in range(len(data)):
    w = opt_weight[i]
    if w >= 0.001:
        self.SetHoldings(df_price.columns[i], w)
def Selection(self) -> None:
if not self.data[self.symbol].is_ready(): return
self.selection_flag = True
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']

#opt_sharpe = - opt['fun']
#opt_weights = opt['x']
#opt_return = self.annual_port_return(opt_weights)
#opt_volatility = self.annual_port_vol(opt_weights)

return opt_weights

class SymbolData():
def __init__(self, algorithm, symbol, period):
self.Symbol = symbol
self.Price = RollingWindow[float](period)
self.Algorithm = algorithm

# Warmup.
history = algorithm.History(algorithm.Symbol(symbol), period, Resolution.Daily)
if not history.empty:
    closes = history.loc[symbol].close
    for time, close in closes.items():
        self.Price.Add(close)
    
def update(self, value):
self.Price.Add(value)

def is_ready(self) -> bool:
return self.Price.IsReady

# performance t12-t6.
def performance(self) -> float:
closes = [x for x in self.Price][:12*21][-6*21:]
return (closes[0] / closes[-1] - 1)
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))