Quant Buffet放轻松,别过度思虑

基于日内收益的月度反转和/或动量策略

登录后收藏

学术论文

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"))