
“该策略通过负日间反转和市值对股票进行排序,做多AB_NR最高五分位的股票,做空最低五分位的股票,投资组合等权重,每月重新平衡。”
资产类别: 股票 | 地区: 美国 | 周期: 每月 | 市场: 股票 | 关键词: 看多、看空
I. 策略概要
投资范围包括在纽约证券交易所、美国证券交易所和纳斯达克上市的普通股,不包括金融和公用事业公司。该策略将每日股票回报分解为隔夜和日间部分。日间回报(RETOC)计算为市场开盘价和收盘价之间的相对价格变化。隔夜回报(RETCO)根据日间回报和标准日回报推算。当正的隔夜回报之后是负的日间回报时,即识别出负日间反转。股票根据负日间反转的频率(AB_NR)和市值(SIZE)进行排序。对大盘股中AB_NR最高的五分位建立多头头寸,对AB_NR最低的五分位建立空头头寸。该策略每月重新平衡。
II. 策略合理性
该论文研究了美国股票中负日间反转现象,即正的隔夜回报之后是负的日间回报,这表明噪音交易者和日间投资者之间存在拉锯战。这种模式反映了噪音交易者在隔夜期间的价格压力,被日间交易的投资者逆转。负日间反转的频率被证明可以预测更高的未来回报,以补偿与噪音交易者相关的风险。这种关系是不对称的,因为高开盘价之后是负日间反转可以预测未来的股票回报,反之则不然。该论文将此归因于噪音交易者在隔夜期间的影响,零售投资者在这些月份的交易中占更大比例。它得出结论,噪音交易者风险解释了回报溢价,因为这些反转的更高频率需要更高的溢价,因为与噪音交易者交易存在风险。由于市场中这种持续存在的模式,该策略仍然有利可图。
III. 来源论文
Overnight Returns, Daytime Reversals, and Future Stock Returns: The Risk of Investing in a Tug of War With Noise Traders [点击查看论文]
- 费尔哈特·阿克巴斯(Ferhat Akbas)、埃克哈特·博默(Ekkehart Boehmer)、蒋超(Chao Jiang)、保罗·D·科赫(Paul D. Koch),伊利诺伊大学芝加哥分校商学院,新加坡管理大学李光前商学院,南卡罗来纳大学金融系,爱荷华州立大学金融系
<摘要>
一个月内正隔夜回报之后是负交易日反转的频率越高,表明对立投资者群体之间每日拉锯战越激烈,这些群体很可能由隔夜的噪音交易者和日间的套利者组成。我们表明,更激烈的每日拉锯战预示着横截面中未来更高的回报。额外的测试支持以下结论:在更激烈的拉锯战中,日间套利者更有可能低估隔夜出现利好消息的可能性,从而过度纠正持续的隔夜上涨价格压力。


IV. 回测表现
| 年化回报 | 5.28% |
| 波动率 | 6.43% |
| β值 | 0.002 |
| 夏普比率 | 0.82 |
| 索提诺比率 | -0.447 |
| 最大回撤 | N/A |
| 胜率 | 49% |
V. 完整的 Python 代码
import numpy as np
from AlgorithmImports import *
import pandas as pd
from pandas.core.frame import dataframe
class ImpactOfOvernightReturnsDaytimeReversals(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
market:Symbol = self.AddEquity("SPY", Resolution.Daily).Symbol
self.fundamental_count:int = 1000
self.fundamental_sorting_key = lambda x: x.DollarVolume
self.period:int = 13 * 21
self.quantile:int = 10
self.leverage:int = 5
self.min_share_price:float = 5.
self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']
self.long:List[Symbol] = []
self.short:List[Symbol] = []
self.selection_flag = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.Schedule.On(self.DateRules.MonthEnd(market), self.TimeRules.AfterMarketOpen(market), 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 and \
x.Market == 'usa' and x.MarketCap != 0
]
if len(selected) > self.fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
AB_NR:Dict[Fundamental, float] = {}
for stock in selected:
symbol:Symbol = stock.Symbol
hist:dataframe = self.History([symbol], self.period, Resolution.Daily)
if 'close' in hist.columns and 'open' in hist.columns:
closes:pd.Series = hist['close']
opens:pd.Series = hist['open']
if len(closes) == self.period and len(opens) == self.period:
# Calculate overnight and daily returns
RET_OC:pd.Series = pd.Series(closes / opens - 1) # Open to close return
RET:pd.Series = pd.Series(closes).pct_change() # Close to close return
RET_CO:pd.Series = ((1 + RET) / (1 + RET_OC)) - 1
# Negative daytime reversal signal for last year
reversal_vector:List = [1 if co > 0 and oc < 0 else 0 for co, oc in zip(RET_CO, RET_OC)]
# Slice it for every month
reversal_separate_months:List = [reversal_vector[x:x+21] for x in range(0, len(reversal_vector),21)]
NRIT:List = [month.count(1) / len(month) for month in reversal_separate_months]
NRIT_current_month:float = NRIT[-1]
NRTI_avg:float = np.average(NRIT[:-2])
# AB_NR calc
AB_NR[stock] = NRIT_current_month / NRTI_avg
if len(AB_NR) != 0:
# Sort by market cap and AB_NR
market_cap_values:List[float] = [x.MarketCap for x in AB_NR.keys()]
high_by_market_cap:List[Fundamental] = [x[0] for x in AB_NR.items() if x[0].MarketCap >= np.percentile(market_cap_values, 66)]
abnr_values:List[float] = list(AB_NR.values())
high_by_abnr:List[Fundamental] = [x[0] for x in AB_NR.items() if x[1] >= np.percentile(abnr_values, 80)]
low_by_abnr:List[Fundamental] = [x[0] for x in AB_NR.items() if x[1] <= np.percentile(abnr_values, 20)]
self.long = [x.Symbol for x in high_by_market_cap if x in high_by_abnr]
self.short = [x.Symbol for x in high_by_market_cap if x in low_by_abnr]
return self.long + self.short
def OnData(self, data: Slice) -> None:
if not self.selection_flag:
return
self.selection_flag = False
# order execution
targets:List[PortfolioTarget] = []
for i, portfolio in enumerate([self.long, self.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.long.clear()
self.short.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"))