“该策略交易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 [点击查看论文]

<摘要>

我们证明了短期小公司溢价与未来低贝塔异常表现之间存在很强的关系。小公司价格的上涨(下跌)暂时改善(恶化)了融资条件,从而有利于(损害)低贝塔策略的短期回报。为了研究这种现象,我们研究了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

发表评论

了解 Quant Buffet 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读