作 者:老余捞鱼
原创不易,转载请标明出处及原作者。

文中示例仅用于技术讨论,不构成任何操作建议。
量化策略开发应以学习和技术交流为目的。
本号不荐股、不卖课、不承诺收益。
市场有风险,请合法合规投资。
本文约 3400 字 | 预计阅读 5-8 分钟
· · ·
你有没有想过,真正给你赚钱的,其实是你睡觉的那段时间?这个事听起来反常识,但数据不会骗人。
我把标普500 ETF,也就是 SPY 的每日收益,拆成了两段来看。
第一段叫隔夜。就是你今天收盘的时候在竞价阶段建仓,然后一直拿到第二天开盘竞价平仓。中间隔着交易所关门的十七个半小时,你没看盘,没操作,账户就放在那里。
第二段叫日内。就是你开盘的时候建仓,收盘的时候平仓。这段时间交易所是开门营业的,有成交量、有新闻、有价格发现,股票市场的整套机器都在跑。
从 1993 年 1 月到 2026 年 8 月,整整 8,437 个交易日,33 年。
我直接说结果,你可能会愣了一下。
日内这段,夏普比率只有 0.128,年化收益 0.79%,算下来一天平均 79 个基点。33 年下来,几乎等于零。
隔夜这段呢?年化 10.05%,夏普 0.957。而你们最熟悉的建仓持有策略,年化 10.91%,夏普 0.784。
也就是说,这 33 年里,SPY 涨出来的那些钱,几乎全是在夜里到账的。你白天盯的那六个半小时,基本上是在陪跑。

这个发现本身不新鲜,在学术文献里躺了二十多年,而且一直在起作用。真正有意思的是,你想拿它做点什么的时候,会发生什么。这个问题的答案,是一个精确到小数点的数字,我放在后面说。
一个很容易踩的坑
在回测之前,有个细节必须先说清楚。搞错了这个,你可能连方向都能看反。
除息日,股票分红的时候,开盘价会自动往下掉一个股息的量。如果你直接用原始价格算隔夜收益,除息日那天,你就把整份股息从隔夜窗口里扣掉了,但没有任何地方把它补回来。结果就是隔夜序列被系统性地低估了,日内序列把差额吃进去。
对 SPY 来说,这个误差每年大约 1.96 个百分点。用错误算法算出来是 8.09%,真实数字是 10.05%。差将近两个点,但结论方向没变。
但对另一只 ETF,也就是 TLT,就是那个长期国债 ETF,用错误算法算出来的隔夜收益是每年负 3.00%。修正之后,是正 0.40%。
符号直接反了。
你要是拿原始价格去算债券基金的隔夜溢价,你会自信满满地写一篇报告说”久期资产的隔夜收益是负的”,然后绞尽脑汁去解释一个根本不存在的东西。
正确做法是用当天的复权因子把开盘价调整到同一基准,然后做个简单的验证,用这个断言检查。
import numpy as np
import pandas as pd
# ========== 假设 df 已存在,包含 'Open', 'Close', 'Adj Close' 列 ==========
# 计算复权因子(当日调整比例)
f = df["Adj Close"] / df["Close"] # 当天的总收益因子
# 调整后的价格(复权口径)
aO = df["Open"] * f # 调整后开盘价
aC = df["Adj Close"] # 调整后收盘价(已复权)
# 三种收益率
on = aO / aC.shift(1) - 1 # 隔夜收益率(前收 → 今开)
it = aC / aO - 1 # 日内收益率(今开 → 今收)
tot = aC / aC.shift(1) - 1 # 全日总收益率(前收 → 今收)
# 验证收益率分解恒等式: (1+on)*(1+it) == 1+tot
assert np.abs((1 + on) * (1 + it) - (1 + tot)).max() < 1e-10, "收益率分解不成立"最后那行断言很重要。两个收益拆出来,要么能完美合并回去,要么就说明算错了,中间没有第三种可能。每次做这种拆解,我都会加上这个检查,不然报出来的数字我自己不踏实。
真实的回测是什么样
很多量化爱好者拿到隔夜收益序列,第一反应是直接复利成一条曲线,然后说,回测结果很好。但这一步省略了一个最要命的东西,8,437 次往返交易,每一笔都要付价差。
真正的回测,必须让交易成本老老实实出现。
我的做法是把每根日线拆成两根,一根在九点半用复权开盘价,一根在下午四点用复权收盘价。然后在每个四点那根进,每个九点半那根出。这样 vectorbt 会生成 8,437 条真实的成交记录,每一笔都扣手续费。
import pandas as pd
import numpy as np
import vectorbt as vbt
# ========== 假设 raw 是字典,键为标的代码,值为原始 OHLC DataFrame ==========
def interleave(t):
"""
将指定标的的调整后开盘价和收盘价交错排列为时间序列,
开盘价标记在当日 9:30,收盘价标记在当日 16:00。
"""
df = raw[t][["Open", "Close", "Adj Close"]].dropna()
f = df["Adj Close"] / df["Close"]
# 调整后开盘价,时间戳设为 9:30
o = pd.Series(
(df["Open"] * f).values,
index=df.index + pd.Timedelta(hours=9, minutes=30)
)
# 调整后收盘价,时间戳设为 16:00
cl = pd.Series(
df["Adj Close"].values,
index=df.index + pd.Timedelta(hours=16)
)
return pd.concat([o, cl]).sort_index()
# 示例:使用 SPY 数据
px = interleave("SPY")
# 生成交易信号(收盘竞价进场,开盘竞价出场)
is_close = px.index.hour == 16
entries = pd.Series(is_close, index=px.index) # 收盘进场
exits = pd.Series(~is_close, index=px.index) # 开盘出场
entries.iloc[-1] = False # 最后一天不留悬空单
# 使用 vectorbt 构建并评估投资组合
pf = vbt.Portfolio.from_signals(
close=px,
entries=entries,
exits=exits,
init_cash=100_000,
fees=0.00005, # 万0.5手续费
freq="12H" # 12小时频率,用于绩效计算
)
# 查看绩效统计(可选)
print(pf.stats())这个策略零参数,没有回看窗口,没有阈值,没有任何东西需要调优,所以也不存在过拟合。关于它的所有问题,最后都归结到一个变量,就是成本。
每边半个基点,结果惊掉下巴
从 10 万美元起步,策略最终账户里有 106 万。听起来不错对吧?但你猜它光手续费就付了多少?接近 38 万。

这是在每边只收半个基点的前提下算的。半个基点是什么概念?就是 0.005% 手续费就能吃掉你一笔交易的一边。如果你不是机构,竞价执行基本拿不到这个价格。
这个数字说明了一件事,就是一个夏普更高、回撤更小的策略,账户里最后只剩下三分之一。而那个什么都不做的建仓持有策略,最后账户里有 320 万。差距就是这么大。
你可能会想,那这套策略的核心优势到底在哪?答案可能跟你想的不一样。
平均每笔赚钱的夜,赚 0.408%。平均每笔亏钱的夜,亏 0.433%。亏的比赚的大。
这套策略能赢,靠的是亏的次数少、赚的次数多。胜率 55.1%,盈亏比 1.12,一年下来总共 252 个夜晚,慢慢积累出这个优势。
所以它不是那种能抓住大行情的策略,它是薄利多销型的,高频收割,每个夜晚只赚一点点。
另外,它也绝对不是什么低风险策略。最差的一个夜晚亏了 10.46%,2020 年 3 月有一段近乎垂直的 30% 下杀,2002 年到 2003 年有一段 36% 的回撤。
你持有隔夜,就是在持有跳空风险。跳空风险,就是这个策略被付钱的原因。
决定一切的,是一个数字
因为这个策略每天固定交易一次,盈亏平衡的成本可以直接算出来,就是隔夜平均收益,换算成基点。
4.02 个基点,往返。
注意,这个成本没有任何办法摊薄。你没法通过减少交易次数来降低成本,因为每天交易一次就是这个策略的定义,不是你选的参数。
把这个成本变一变,结果是这样的。
零成本的时候,夏普 1.15,建仓持有是 0.78,而且策略的回撤比建仓持有少 35%。一个基点往返的时候,策略还是赢。两个基点,就不赢了。四个基点,直接归零。
这就是”成本受限”的策略。你看到的优势能不能装进你口袋,取决于你的执行成本能不能控制在 4.02 个基点以内。这不是调参数能解决的问题,是你找哪家券商、用什么方式下单决定的。
它还没有消失
对任何一个二十年前就存在的异象,大家第一个问题往往是,这个东西还存在,早就被套利没了吧?
我把 SPY 的隔夜收益按五年一档拆开看了一下。六个档,每一档都是正的。最差的是 2000 年到 2004 年,那是科技泡沫破裂那段时间。最近三档全部处于或高于全样本均值。
这个效应在最近这些年反而更稳定了,不是更弱了。
这个持续性本身就是一个信号。错误定价一旦被发现,大家都会来套利,套着套着就没了。但风险补偿不一样,总得有人承担那个风险。这笔交易不会消解它自己的成因,因为风险本身是真实存在的。
我还试了另一个优化方向,失败了
我试了另外一个思路是这样的。风险转移的逻辑认为,交易商在下跌交易日结束后,手里会积累更多卖不掉的仓位,所以跌日之后的隔夜溢价应该更高。同时,如果只在跌日之后做,交易的频率能砍一半,这对成本敏感的策略来说很有吸引力。
结果呢?跌日之后隔夜收益 4.405 个基点,涨日之后是 3.689 个基点,差 0.716 个基点,t 统计量只有 0.49。
统计学上,这等于没有。
更宽范围的 ETF 测试里,连方向都不一致。
我还是把这个结果报出来,因为这种事被悄悄丢掉是最亏的。下一个想做这个方向的人,应该直接搜到这个结果,而不是用稍微不同的阈值再试一遍然后失望。
观点总结
我不会直接拿本文描述的内容当策略用。在散户的竞价执行成本下,这个发现只能算一项研究,不是可以直接上线的策略。
但它能改变你已经在跑的东西的执行方式。
如果你手里有类似 SPY 的仓位,而且是你自己在决定什么时候再平衡,这个拆解告诉你一件事,日内那六个半小时是风险所在,收益几乎不在那里。
所以,把再平衡的时间挪到收盘,而不是开盘。这是免费的,不多占用任何换手,因为反正你本来就要动。
这才是这个发现真正有价值的用法,而且任何人都能用,不挑资金量。
顺便说,这个结论也重新定义了一下什么叫长期持有多头仓位。你并不是因为”拥有市场”而被付钱,你是因为”没法在下午四点到次日九点半之间卖掉它”而被付钱。剩下的一切,都是带着 15.2% 标准差的噪声。
#隔夜效应 #日内效应 #标普500 #SPY #量化回测 #风险溢价 #跳空风险 #总收益 #交易成本 #除息日 #数据陷阱 #vectorbt
Be First to Comment