
“该策略交易24个发达市场,做多高SMB回报国家的BAB策略,做空低SMB回报国家的BAB策略,使用等权重和每月重新平衡进行绩效优化。”
资产类别: 股票 | 地区: 全球 | 周期: 每月 | 市场: 股票 | 关键词: 贝塔、小盘股
I. 策略概要
该策略的目标是AQR因子覆盖的24个发达市场的股票。每个月,国家按其过去t-3月至t-1月的几何平均SMB(小盘股减大盘股)回报进行排名。选择前20%(最高SMB回报)和后20%(最低SMB回报)。在高SMB回报国家构建BAB(反贝塔)策略的多头投资组合,在低SMB回报国家构建BAB策略的空头投资组合。投资组合等权重,每月重新平衡,利用SMB回报的跨国差异来提高绩效。
II. 策略合理性
作者将资产流动性(从小公司短期业绩中可以看出)与影响低贝塔策略的融资流动性联系起来。BAB投资组合大量暴露于小盘股,因此小盘股价格的上涨改善了融资条件。价格上涨提高了用于保证金交易的抵押品的价值,使交易者(对冲基金、交易商等)能够借入更多资金,从而增加了对低贝塔股票的需求。在BAB策略中,低贝塔股票被杠杆化,以使多头的贝塔等于1,从而产生进一步的需求,并暂时提高价格和回报。流动性风险的降低提高了小盘股减大盘股(SMB)正收益后的BAB表现,但在这些时期之外,BAB产生的阿尔法极小。
III. 来源论文
Small-Minus-Big Predicts Betting-Against-Beta: Implications for International Equity Allocation and Market Timing [点击查看论文]
- 亚当·扎伦巴(Adam Zaremba),蒙彼利埃商学院;波兹南经济与商业大学;开普敦大学(UCT)
<摘要>
我们证明了短期小公司溢价与未来低贝塔异常表现之间存在很强的关系。小公司价格的上涨(下跌)暂时改善(恶化)了融资条件,从而有利于(损害)低贝塔策略的短期回报。为了研究这种现象,我们研究了1989年至2018年间24个发达市场中反贝塔(BAB)和小盘股减大盘股(SMB)因子投资组合的回报。在三个月SMB回报最高的(最低的)五分之一国家中做多(做空)BAB因子的零投资策略产生了每月1.46%的平均回报。该效应对于控制股票市场中的主要风险因子、替代投资组合构建方法和子周期分析都是稳健的。SMB回报对BAB表现的可预测性也存在于单个国家回报的时间序列中,为低贝塔策略的有效择时奠定了基础。


IV. 回测表现
| 年化回报 | 22.42% |
| 波动率 | 19.87% |
| β值 | -0.033 |
| 夏普比率 | 1.13 |
| 索提诺比率 | N/A |
| 最大回撤 | N/A |
| 胜率 | 55% |
V. 完整的 Python 代码
from AlgorithmImports import *
import numpy as np
#endregion
class TimingBettingAgainstBetawithSmallStocks(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.countries = [
"AUS", "AUT", "BEL", "CAN", "DNK", "FIN", "FRA", "DEU",
"GRC", "HKG", "IRL", "ISR","ITA","JPN","NLD","NZL","NOR",
"PRT","SGP","ESP","SWE","CHE","GBR","USA"
]
self.symbol:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.smb_history:dict = {}
self.quantile:int = 5
self.max_missing_days:int = 5
self.period:int = 3 * 21 # performance period.
self.SetWarmUp(self.period, Resolution.Daily)
self.smb_symbol:Symbol = self.AddData(SMB, 'SMB_percentage', Resolution.Daily).Symbol
for country in self.countries:
# BAB and SMB data.
data = self.AddData(BAB, country + '_BAB', Resolution.Daily)
data.SetLeverage(10)
data.SetFeeModel(CustomFeeModel())
self.smb_history[country] = RollingWindow[float](self.period)
self.recent_month:int = -1
def OnData(self, data:Slice) -> None:
if self.smb_symbol in data and data[self.smb_symbol]:
for country in self.countries:
smb_value:float = data[self.smb_symbol].GetProperty(country)
self.smb_history[country].Add(smb_value)
# rebalance monthly
if self.Time.month == self.recent_month:
return
self.recent_month = self.Time.month
# SMB factor data is still comming in
if self.Securities[self.smb_symbol].GetLastData() and (self.Time.date() - self.Securities[self.smb_symbol].GetLastData().Time.date()).days > self.max_missing_days:
self.Liquidate()
return
# calculate average performance
avg_perf:dict[str, float] = {}
for country in self.countries:
if self.smb_history[country].IsReady:
# BAB factor data is still comming in
if self.Securities[country + '_BAB'].GetLastData() and (self.Time.date() - self.Securities[country + '_BAB'].GetLastData().Time.date()).days > self.max_missing_days:
continue
avg_perf[country] = np.average([x for x in self.smb_history[country]])
if len(avg_perf) < self.quantile:
self.Liquidate()
return
sorted_by_avg_perf:List = [x[0] for x in sorted(avg_perf.items(), key = lambda x: x[1], reverse = True)]
quantile:int = int(len(sorted_by_avg_perf) / self.quantile)
long_countries:List[str] = sorted_by_avg_perf[:quantile]
short_countries:List[str] = sorted_by_avg_perf[-quantile:]
invested:List[str] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in long_countries + short_countries:
self.Liquidate(self.traded_symbol(symbol))
long_count:int = len(long_countries)
short_count:int = len(short_countries)
for country in long_countries:
if self.traded_symbol(country) in data and data[self.traded_symbol(country)]:
self.SetHoldings(self.traded_symbol(country), 1 / long_count)
for country in short_countries:
if self.traded_symbol(country) in data and data[self.traded_symbol(country)]:
self.SetHoldings(self.traded_symbol(country), -1 / short_count)
def traded_symbol(self, symbol:str) -> str:
return symbol + '_BAB'
# Custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))
# SMB factor.
# NOTE: IMPORTANT: Data order must be ascending (datewise).
# Data source: https://www.aqr.com/Insights/Datasets/Betting-Against-Beta-Equity-Factors-Daily
class SMB(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource("data.quantpedia.com/backtesting_data/equity/smb_factor_percentage.csv", SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
# File example.
# DATE;AUS;AUT;BEL;CAN;CHE;DEU;DNK;ESP;FIN;FRA;GBR;GRC;HKG;IRL;ISR;ITA;JPN;NLD;NOR;NZL;PRT;SGP;SWE;USA
# 09/30/2020;1.40;1.19;0.72;-0.22;0.84;1.05;0.31;1.26;0.67;0.83;1.12;-0.16;-0.47;0.16;-0.20;1.21;-0.07;0.26;0.23;-0.37;0.68;-0.84;0.43;-0.61
def Reader(self, config, line, date, isLiveMode):
data = SMB()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(';')
# Prevent look-ahead bias.
data.Time = datetime.strptime(split[0], "%m/%d/%Y") + timedelta(days=1)
data['AUS'] = float(split[1])
data['AUT'] = float(split[2])
data['BEL'] = float(split[3])
data['CAN'] = float(split[4])
data['CHE'] = float(split[5])
data['DEU'] = float(split[6])
data['DNK'] = float(split[7])
data['ESP'] = float(split[8])
data['FIN'] = float(split[9])
data['FRA'] = float(split[10])
data['GBR'] = float(split[11])
data['GRC'] = float(split[12])
data['HKG'] = float(split[13])
data['IRL'] = float(split[14])
data['ISR'] = float(split[15])
data['ITA'] = float(split[16])
data['JPN'] = float(split[17])
data['NLD'] = float(split[18])
data['NOR'] = float(split[19])
data['NZL'] = float(split[20])
data['PRT'] = float(split[21])
data['SGP'] = float(split[22])
data['SWE'] = float(split[23])
data['USA'] = float(split[24])
data.Value = float(split[1])
return data
# BAB factor.
# NOTE: IMPORTANT: Data order must be ascending (datewise).
# Data source: https://www.aqr.com/Insights/Datasets/Betting-Against-Beta-Equity-Factors-Daily
class BAB(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource(f"data.quantpedia.com/backtesting_data/equity/{config.Symbol.Value}.csv", SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
# File example.
# date;AUS
# 09/30/2020;24.05738634
def Reader(self, config, line, date, isLiveMode):
data = BAB()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(';')
# Prevent look-ahead bias.
data.Time = datetime.strptime(split[0], "%m/%d/%Y") + timedelta(days=1)
data.Value = float(split[1])
return data