小型行业溢价
登录后收藏学术论文
策略概要
该策略专注于来自51个国家(包括发达市场、新兴市场和前沿市场)的936个本地超级行业指数,涵盖基于行业分类基准(ICB)的19个行业。市场数据以当地货币收集并转换为美元,以一个月期国库券利率作为无风险代理。通过根据行业指数相对于平均市场价值(MV)的横截面排名对其进行加权,构建多空零投资组合。权重通过缩放因子进行调整,以保持美元多头和美元空头平衡。投资组合权重总和为零,确保零投资策略。投资组合每月重新平衡,回报计算为各指数回报的加权总和。这种系统性方法利用横截面排名来提高跨多元化全球行业样本的回报,同时保持风险中性。
II. 策略合理性
小行业和国家市场的回报溢价可能源于行为错价或非市场风险因素的补偿。市场规模(MV)包括四个组成部分:动量(MOM)、长期回报或反转(REV)、综合股权发行(ISS)和60个月滞后的总市值(LMV)。因子投资组合(MOM、REV、ISS、LMV)的成对相关性较低,范围从-0.25到0.19,这支持了规模组成部分解释规模因子本身以外的回报的假设。这些组成部分在预期回报模型中补充了规模,增强了解释力。横截面测试显示,规模溢价不能完全归因于任何单一组成部分,所有四个投资组合均提供显著的原始回报和风险调整后回报。这表明,将规模组成部分与规模一起纳入可以改善回报建模,突显了它们在捕捉跨行业预期回报变化方面的互补作用。
回测表现
波动率12.09%
夏普比率0.83
索提诺比率0.14
胜率70%
完整 Python 代码
from AlgorithmImports import *
import data_tools
# endregion
class SmallIndustryPremia(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.min_share_price:int = 5
self.period:int = 60
self.leverage:int = 5
self.weights:dict = {}
self.data:dict = {}
self.prev_industry_country_ids:list[str] = []
market_symbol:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.selection_flag:bool = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Schedule.On(self.DateRules.MonthStart(market_symbol), self.TimeRules.BeforeMarketClose(market_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]:
if not self.selection_flag:
return Universe.Unchanged
selected:list[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Price >= self.min_share_price]
stocks_by_industry_id:dict = {}
for stock in selected:
market_cap:float = stock.MarketCap
country_id:str = stock.CompanyReference.BusinessCountryID
sector:str = str(stock.AssetClassification.MorningstarSectorCode)
if not country_id or not sector or market_cap == 0:
continue
industry_id:str = country_id + sector
if industry_id not in stocks_by_industry_id:
stocks_by_industry_id[industry_id] = []
stocks_by_industry_id[industry_id].append(stock)
# store not adjusted MV value
for industry_id, stocks in stocks_by_industry_id.items():
MV_value:float = np.mean(list(map(lambda stock: stock.MarketCap, stocks)))
if industry_id not in self.data or industry_id not in self.prev_industry_ids: # require consecutive data
self.data[industry_id] = data_tools.IndustryData(self.period)
self.data[industry_id].update_MV_values(MV_value)
MV_by_industry_id:dict = { industry_id: industry_obj.get_MV() \
for industry_id, industry_obj in self.data.items() if industry_obj.is_ready() }
mean_MV:float = np.mean(list(MV_by_industry_id.values()))
# MV difference from mean
long_mean_diff:dict = { industry_id : MV - mean_MV for industry_id, MV in MV_by_industry_id.items() if MV > mean_MV }
short_mean_diff:dict = { industry_id : MV - mean_MV for industry_id, MV in MV_by_industry_id.items() if MV < mean_MV }
total_mean_diff_long:float = abs(sum(list(long_mean_diff.values())))
total_mean_diff_short:float = abs(sum(list(short_mean_diff.values())))
# calculate weight for each stock in long and short leg
for diff_dict in [long_mean_diff, short_mean_diff]:
# calculate weights that are proportional to distance from the mean
total_mean_diff:float = abs(sum(list(diff_dict.values())))
for industry_id, MV_mean_diff in long_mean_diff.items():
if industry_id in stocks_by_industry_id:
ind_weight:float = MV_mean_diff / total_mean_diff
ind_stocks:list = stocks_by_industry_id[industry_id]
ind_stock_count:float = len(ind_stocks)
for stock in ind_stocks:
self.weights[stock.Symbol] = ind_weight / ind_stock_count
self.prev_industry_ids = list(stocks_by_industry_id.keys())
return list(self.weights.keys())
def OnData(self, data: Slice) -> None:
# rebalance monthly
if not self.selection_flag:
return
self.selection_flag = False
# trade execution
portfolio:List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weights.items() if symbol in data and data[symbol]]
self.SetHoldings(portfolio, True)
self.weights.clear()
def Selection(self) -> None:
self.selection_flag = True
# Custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))