基于日内收益的月度反转和/或动量策略
登录后收藏学术论文
Reversal, Momentum and Intraday Returns
Reversal, Momentum and Intraday Returns [点击查看论文]
- Shanghai University of Finance and Economics
策略概要
该策略的目标是纽约证券交易所、美国证券交易所和纳斯达克上市的国内主要股票,不包括价格低于5美元或属于纽约证券交易所最小规模十分位数的股票。股票根据纽约证券交易所的规模十分位数分为三组(小盘股、中盘股、大盘股),重点关注“大盘股”组。盘中回报使用交易和报价(TAQ)数据库价格计算,比较14:00(P1)和16:00(P2)之前的最后交易价格。每月盘中回报累积,股票根据之前的月度回报分为十分位数。采用逆向策略,买入底部十分位数(输家),卖出顶部十分位数(赢家)。投资组合等权重,并每月重新平衡。
II. 策略合理性
收盘前的交易很大程度上是由流动性驱动的,因为投资者会重新平衡投资组合,以避免隔夜持有次优头寸。流动性提供者会满足这些交易,接受次优头寸以换取更高的预期回报。这使得资产价格在收盘前偏离基本价值。如果流动性需求持续存在,价格可能不会立即修正,一些流动性驱动的价格压力可能会持续数月。这在资产价格中造成了短期低效率,为知情投资者提供了利用流动性动态造成的错误定价的机会。
回测表现
波动率9.21%
夏普比率0.56
索提诺比率0.052
胜率49%
完整 Python 代码
from AlgorithmImports import *
from typing import List, Dict
class MonthlyReversalMomentumBasedIntradayReturns(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
self.min_share_price:int = 5
self.leverage:int = 5
self.quantile:int = 10
self.days_to_lookup:int = 21
self.period:int = self.days_to_lookup * 7
market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.fundamental_sorting_key = lambda x: x.DollarVolume
self.fundamental_count:int = 300
self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']
self.last_course:List[Symbol] = []
# Relevant hourly closes (at 14 and 16)
self.daily_return:Dict[Symbol, float] = {}
self.price_14:Dict[Symbol, float] = {}
self.settings.daily_precise_end_time = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.UniverseSettings.Resolution = Resolution.Hour
self.AddUniverse(self.FundamentalSelectionFunction)
self.selection_flag:bool = False
self.Schedule.On(self.DateRules.MonthEnd(market), self.TimeRules.AfterMarketOpen(market), self.Selection)
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
self.selection_flag = False
selected:List[Fundamental] = [
x for x in fundamental if x.HasFundamentalData and x.Price > self.min_share_price and \
x.Market == 'usa' and x.SecurityReference.ExchangeId in self.exchange_codes\
]
if len(selected) > self.fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
self.last_course = [x.Symbol for x in selected[:self.fundamental_count]]
return self.last_course
def OnData(self, data: Slice):
# Store 14h price to calculate daily return.
if self.Time.hour == 14:
for symbol in self.last_course:
if symbol in data and data[symbol]:
price_14:float = data[symbol].Value
if price_14 != 0:
# 14h price value.
self.price_14[symbol] = price_14
# Calculate daily return.
if self.Time.hour == 16:
accumulated_returns:Dict[Symbol, float] = {}
for symbol in self.last_course:
if symbol in self.price_14 and symbol in data and data[symbol]:
price_16:float = data[symbol].Value
if price_16 != 0:
# Calculate intraday return.
price_14 = self.price_14[symbol] # 14h price
ret:float = price_16 / price_14 - 1
if symbol not in self.daily_return:
self.daily_return[symbol] = RollingWindow[float](21) # One month of daily returns
self.daily_return[symbol].Add(ret)
# Month worth of daily return is ready.
if self.daily_return[symbol].IsReady:
acc_ret:float = sum([x for x in self.daily_return[symbol]])
accumulated_returns[symbol] = acc_ret
if len(accumulated_returns) == 0:
return
# Sort by daily accumulated returns.
sorted_by_return:List[Tuple[Symbol, float]] = sorted(accumulated_returns.items(), key = lambda x: x[1], reverse = True)
quantile:int = int(len(sorted_by_return) / self.quantile)
long:List[Symbol] = [x[0] for x in sorted_by_return[-quantile:]]
short:List[Symbol] = [x[0] for x in sorted_by_return[:quantile]]
# Trade execution
targets:List[PortfolioTarget] = []
for i, portfolio in enumerate([long, short]):
for symbol in portfolio:
if symbol in data and data[symbol]:
targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
self.SetHoldings(targets, True)
self.daily_return.clear()
def Selection(self):
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"))