已实现偏度预测股票回报
登录后收藏学术论文
Do Realized Skewness and Kurtosis Predict the Cross-Section of Equity Returns?
已实现偏度是否预测股票回报的横截面?[点击查看论文]
- CAWilfrid Laurier University
- CAUniversity of Toronto
- DKCopenhagen Business School
- DKAarhus University
- ?Aarhus University - CREATES
- ?University of Toronto - Rotman School of Management
- University of Houston
- ?University of Houston - C.T. Bauer College of Business
- MXInstituto Tecnológico Autónomo de México
- ?Instituto Tecnológico Autónomo de México (ITAM) - Department of Business Administration
策略概要
投资范围包括价格高于5美元的纽约证券交易所、美国证券交易所和纳斯达克股票,重点关注规模最大的五分之一。盘中5分钟报价计算对数回报,并从该数据中得出偏度,汇总为每周实现的偏度。负偏度表示左偏回报分布,而正偏度表示右偏分布。每周,股票根据实现的偏度被分为十分位数。投资者买入最高十分位数的股票,卖出最低十分位数的股票,创建一个每周再平衡的价值加权投资组合。该策略利用盘中数据来利用偏度驱动的回报异常,以获得系统性的交易机会。
II. 策略合理性
研究得出的结论是,在累积前景理论或使用异质投资者偏好偏度理论的情况下,偏度越大的资产回报越低。
回测表现
波动率11.53%
夏普比率0.84
索提诺比率0.523
胜率100%
完整 Python 代码
from AlgorithmImports import *
from collections import deque
from scipy.stats import skew
import numpy as np
from pandas.core.frame import DataFrame
#endregion
class RealizedSkewnessPredictsEquityReturns(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
market:Symbol = self.AddEquity('SPY', Resolution.Minute).Symbol
# 5-minute price data.
self.data:Dict[Symbol, deque] = {}
self.period:int = 5 * 78
self.rebalance_month:int = 12
self.leverage:int = 5
self.quantile:int = 10
self.min_share_price:float = 5.
self.fundamental_count:int = 100
self.fundamental_sorting_key = lambda x: x.MarketCap
# Yearly selected universe with symbol and market cap data.
self.selected_universe:List[Fundamental] = []
self.month:int = 12
self.days:int = 5
self.selection_flag:bool = False
self.UniverseSettings.Resolution = Resolution.Minute
self.AddUniverse(self.FundamentalSelectionFunction)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.settings.daily_precise_end_time = False
self.Schedule.On(self.DateRules.MonthStart(market), self.TimeRules.AfterMarketOpen(market), self.Selection)
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
symbol = security.Symbol
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(self.leverage)
if symbol not in self.data:
history:DataFrame = self.History(symbol, self.period * 5, Resolution.Minute)
if len(history) == self.period and 'close' in history:
closes_1M:List[float] = list(history['close'])
closes_5M:List[float] = closes_1M[::5]
self.data[symbol] = deque(closes_5M, maxlen = self.period)
# Remove old stocks from selected universe data.
for security in changes.RemovedSecurities:
if security.Symbol in self.data:
del self.data[security.Symbol]
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
if not self.selection_flag:
return Universe.Unchanged
self.selection_flag = False
selected:List[Fundamental] = [
x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.Price >= self.min_share_price
]
if len(selected) > self.fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
self.selected_universe = selected
return list(map(lambda x: x.Symbol, selected))
def OnData(self, data: Slice) -> None:
if self.Time.minute % 5 == 0:
# Store 5 minute data.
for symbol in self.data:
if symbol in data and data[symbol]:
price = data[symbol].Value
self.data[symbol].append(price)
if not (self.Time.hour == 16 and self.Time.minute == 0):
return
if self.days == 5:
aggregate_skewness:Dict[Symbol, float] = {}
for stock in self.selected_universe:
symbol:Symbol = stock.Symbol
# 5 Minute data is ready.
if symbol in self.data and len(self.data[symbol]) == self.data[symbol].maxlen:
closes_5M:np.ndarray = np.array(self.data[symbol])
returns_5M:np.ndarray = (closes_5M[1:] - closes_5M[:-1]) / closes_5M[:-1]
skewness:float = skew(returns_5M)
aggregate_skewness[stock] = skewness
if len(aggregate_skewness) != 0:
# Aggregate skewness sorting.
sorted_by_aggregate_skewness:List = sorted(aggregate_skewness.items(), key = lambda x: x[1], reverse = True)
quantile:int = int(len(sorted_by_aggregate_skewness) / self.quantile)
long:List[Fundamental] = [x[0] for x in sorted_by_aggregate_skewness[-quantile:]]
short:List[Fundamental] = [x[0] for x in sorted_by_aggregate_skewness[:quantile]]
weight:Dict[Symbol, float] = {}
# Market cap weighting.
for i, portfolio in enumerate([long, short]):
mc_sum:float = sum(map(lambda x: x.MarketCap, portfolio))
for stock in portfolio:
weight[stock.Symbol] = ((-1) ** i) * stock.MarketCap / mc_sum
# Trade execution.
portfolio:List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in weight.items() if symbol in data and data[symbol]]
self.SetHoldings(portfolio, True)
self.days += 1
if self.days > 5:
self.days = 1
def Selection(self) -> None:
if self.month == self.rebalance_month:
self.selection_flag = True
self.month += 1
if self.month > 12:
self.month = 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"))from AlgorithmImports import *
from collections import deque
from scipy.stats import skew
import numpy as np
from pandas.core.frame import DataFrame
#endregion
class RealizedSkewnessPredictsEquityReturns(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
market:Symbol = self.AddEquity('SPY', Resolution.Minute).Symbol
# 5-minute price data.
self.data:Dict[Symbol, deque] = {}
self.period:int = 5 * 78
self.rebalance_month:int = 12
self.leverage:int = 5
self.quantile:int = 10
self.min_share_price:float = 5.
self.fundamental_count:int = 100
self.fundamental_sorting_key = lambda x: x.MarketCap
# Yearly selected universe with symbol and market cap data.
self.selected_universe:List[Fundamental] = []
self.month:int = 12
self.days:int = 5
self.selection_flag:bool = False
self.UniverseSettings.Resolution = Resolution.Minute
self.AddUniverse(self.FundamentalSelectionFunction)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.settings.daily_precise_end_time = False
self.Schedule.On(self.DateRules.MonthStart(market), self.TimeRules.AfterMarketOpen(market), self.Selection)
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
symbol = security.Symbol
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(self.leverage)
if symbol not in self.data:
history:DataFrame = self.History(symbol, self.period * 5, Resolution.Minute)
if len(history) == self.period and 'close' in history:
closes_1M:List[float] = list(history['close'])
closes_5M:List[float] = closes_1M[::5]
self.data[symbol] = deque(closes_5M, maxlen = self.period)
# Remove old stocks from selected universe data.
for security in changes.RemovedSecurities:
if security.Symbol in self.data:
del self.data[security.Symbol]
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
if not self.selection_flag:
return Universe.Unchanged
self.selection_flag = False
selected:List[Fundamental] = [
x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.Price >= self.min_share_price
]
if len(selected) > self.fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
self.selected_universe = selected
return list(map(lambda x: x.Symbol, selected))
def OnData(self, data: Slice) -> None:
if self.Time.minute % 5 == 0:
# Store 5 minute data.
for symbol in self.data:
if symbol in data and data[symbol]:
price = data[symbol].Value
self.data[symbol].append(price)
if not (self.Time.hour == 16 and self.Time.minute == 0):
return
if self.days == 5:
aggregate_skewness:Dict[Symbol, float] = {}
for stock in self.selected_universe:
symbol:Symbol = stock.Symbol
# 5 Minute data is ready.
if symbol in self.data and len(self.data[symbol]) == self.data[symbol].maxlen:
closes_5M:np.ndarray = np.array(self.data[symbol])
returns_5M:np.ndarray = (closes_5M[1:] - closes_5M[:-1]) / closes_5M[:-1]
skewness:float = skew(returns_5M)
aggregate_skewness[stock] = skewness
if len(aggregate_skewness) != 0:
# Aggregate skewness sorting.
sorted_by_aggregate_skewness:List = sorted(aggregate_skewness.items(), key = lambda x: x[1], reverse = True)
quantile:int = int(len(sorted_by_aggregate_skewness) / self.quantile)
long:List[Fundamental] = [x[0] for x in sorted_by_aggregate_skewness[-quantile:]]
short:List[Fundamental] = [x[0] for x in sorted_by_aggregate_skewness[:quantile]]
weight:Dict[Symbol, float] = {}
# Market cap weighting.
for i, portfolio in enumerate([long, short]):
mc_sum:float = sum(map(lambda x: x.MarketCap, portfolio))
for stock in portfolio:
weight[stock.Symbol] = ((-1) ** i) * stock.MarketCap / mc_sum
# Trade execution.
portfolio:List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in weight.items() if symbol in data and data[symbol]]
self.SetHoldings(portfolio, True)
self.days += 1
if self.days > 5:
self.days = 1
def Selection(self) -> None:
if self.month == self.rebalance_month:
self.selection_flag = True
self.month += 1
if self.month > 12:
self.month = 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"))