一句话结论:这周开盘价和上周收盘价之间有缺口时,79.2% 的周会在这一周之内碰回上周的收盘价,一半以上(54.4%)第一天就碰回。
缺口越小越容易补:小于平均每周波幅 10% 的缺口,95% 当周就补上;超过 30% 的,只剩一半多。
作用:周一开盘离上周收盘不远时,上周收盘价是这周最可靠的目标位之一;如果到第三个交易日(一般是周三)收盘还没碰到,这周大概率就不会碰了。
周一开盘,ES 常常开在离上周五收盘不远的地方,中间留下一个缺口。上周的收盘价,这周会不会被碰回来?多久碰回来?我用 ES 2010–2026 年清洗后的日盘数据统计了一遍,再加了一个日线版本:昨天的收盘价,今天会不会被碰回来。
2010–2026 年 · 上周收盘价在这周被碰回
79.2%
样本 768 个缺口 · 第一天就碰回 54.4%
按缺口大小缺口大小 = |这周开盘价 − 上周收盘价| ÷ 过去 8 周平均每周的波幅(最高 − 最低)。柱子上的数字是当周碰回的比例。
从开盘起,多久碰回横轴是这周开盘以后的时间(每天日盘 81 根 5 分钟 K 线),纵轴是已经碰回的缺口占全部缺口的比例;虚线是第一天收盘。
只看日盘(美东 09:30–16:15)。碰回 = 某根 K 线的最高价和最低价把上周(昨天)的收盘价夹在中间。前后两周(两天)不是同一个合约的跳过。鼠标移到图上(手机上点一下)看具体数字。
有缺口的周,八成会碰回
- 16 年里一共 768 个周线缺口(向上 420 个,向下 348 个);开盘正好等于上周收盘、没有缺口的只有 10 周。
- 当周碰回上周收盘价的有 79.2%。
- 碰回的里面,68.8% 是第一天就碰回的,84.7% 在前两天之内。
所以有缺口的周里,大约八成会在当周回到上周的收盘价,而且大多回得很快。
缺口越大,越不容易补
缺口大小用”过去 8 周平均每周的波幅”来量,这样 2010 年和 2026 年的点位才能放在一起比。
| 缺口大小 | 缺口数 | 当周碰回 |
|---|---|---|
| 0–5% | 149 | 94.0% |
| 5–10% | 162 | 95.7% |
| 10–15% | 121 | 77.7% |
| 15–20% | 89 | 76.4% |
| 20–30% | 121 | 67.8% |
| 30–50% | 91 | 57.1% |
| 50–100% | 24 | 54.2% |
| 100% 以上 | 11 | 36.4% |
- 10% 是一个分界:小于 10% 的缺口几乎都会补(95%);过了 10%,一下掉到 77% 左右。
- 超过 30% 就只剩一半多:这时上周收盘价离得太远,不能再当成”大概率会到”的目标。
- 最后两档样本很少(24 个和 11 个),比例只能看个大概。
什么时候补:大多在第一天
- 第一天就碰回的占 54.4%,也就是所有回补里的 68.8%。其中第一根 5 分钟 K 线就碰回的有 12.6%,第一个小时内 36.7%。
- 到第三天收盘,已经有 74.2% 碰回,占所有回补的 93.8%。
- 第三天收盘还没碰回的缺口(25.8%),只有大约五分之一会在周四、周五补上。
所以上周收盘价的”吸引力”主要在这周的前两三天;到第三个交易日收盘还没碰到,就不要再指望它了。
向下缺口更容易补
向下缺口(这周低开)当周碰回 83.6%,向上缺口(高开)只有 75.5%。
一个可能的原因是 ES 这 16 年大部分时间在上涨:低开以后被买回去的情况更多,高开以后有时就一路往上,不再回头。
日线版本:昨天的收盘价
切换到”日线”,规则不变,只是把”周”换成”天”:今天开盘价和昨天收盘价之间有缺口,今天日盘会不会碰回昨天的收盘价。
- 3774 个缺口里,当天碰回 61.6%,比周线低不少。
- 开盘第一根 5 分钟 K 线就碰回 16.2%,第一个小时 41.3%,到 11:30 是 49.3%。之后碰回得越来越慢。
- 同样是越小越容易补:小于平均每天波幅 10% 的缺口,95% 当天补上;50%–100% 的只有 34.5%;超过一整天波幅的,只有 11.7%。
- 向下缺口 64.2%,向上缺口 59.5%,方向和周线一样。
日线的回补率低,主要是因为相对于一天的波幅,隔夜的缺口往往更大:有 1865 个缺口(将近一半)超过了平均每天波幅的 30%,而周线只有 126 个(16%)。
逐年
周线每年只有 48 个左右的缺口,逐年的数字跳得比较厉害:最低的 2020 年 59.6%,最高的 2023 年 91.7%。看单独一年的时候要注意样本很小。
定义和复现
缺口回补
- 数据:清洗后的 ES 1 分钟 K 线,2010-06 到 2026-09,清洗方法见这篇。
- 日盘:美东 09:30–16:15,也就是 1 分钟 K 线从 09:30 到 16:14。不含夜盘:如果算上夜盘,周日晚上一开盘缺口就被填掉了一部分,周线缺口会小很多。
- 这周开盘价:这周第一个交易日 09:30 的开盘价(周一放假就用周二)。上周收盘价:上周最后一个交易日日盘的收盘价。
- 缺口:这周开盘价 − 上周收盘价。正好等于 0 的不算(10 周)。
- 碰回:从这周开盘起,某根 K 线的最高价和最低价把上周收盘价夹在中间。向上缺口看最低价有没有跌到上周收盘价,向下缺口看最高价。
- 缺口大小:|缺口| ÷ 过去 8 周平均每周的波幅(每周日盘最高 − 最低)。
- 跳过:上一周和这一周不是同一个合约的(换合约的那一周),不比较。
- 日线版本:昨天 = 上一个股市交易日,规则和突破昨日高低点那篇一样;平均波幅用过去 8 天;“一开始就碰回”改成开盘第一个小时(09:30–10:30)。
我有一份清洗过的 ES 期货 1 分钟 K 线 CSV(Databento 的 ES.c.0 连续合约,列有 ts_event、instrument_id、open、high、low、close,时间是 UTC)。请帮我写 Python 代码,统计“上周(昨天)的收盘价,这周(今天)有没有被碰回来”。规则如下: 1. 时间先转成美东时间(US/Eastern),自动处理夏令时。只看日盘 09:30–16:15,也就是 1 分钟 K 线从 09:30 到 16:14,不含夜盘。 2. 每天的开盘价 = 09:30 那根的开盘价,收盘价 = 16:14 那根的收盘价,最高价 / 最低价 = 日盘所有 K 线的最高 / 最低。 3. 周线:一周 = 周一到周五。这周开盘价 = 这周第一个交易日的开盘价;上周收盘价 = 上周最后一个交易日的收盘价。缺口 = 这周开盘价 − 上周收盘价,正好等于 0 的不算。 4. 碰回:从这周开盘起,只要有一天(一根 K 线)的最高价和最低价把上周收盘价夹在中间就算。向上缺口看最低价 ≤ 上周收盘价,向下缺口看最高价 ≥ 上周收盘价。再记一下是不是第一天就碰回。 5. 缺口大小 = |缺口| ÷ 过去 8 周平均每周的波幅(每周最高 − 最低),分成 0–5%、5–10%、10–15%、15–20%、20–30%、30–50%、50–100%、100% 以上 8 档。 6. 跳过:上一周数据缺失的;上周最后一天和这周第一天的 instrument_id 不同(换了合约)的。一周中间换了合约的话,换约以后的几天不看。 7. 日线:把“周”换成“天”。昨天 = 今天之前最近的一个股市交易日,用纽交所日历判断(pandas 自带的节假日规则,再加上 2012-10-29、2012-10-30、2018-12-05、2025-01-09 临时休市);如果这个昨天不在数据里,或者两天合约不同,今天就跳过。平均波幅用过去 8 天;“一开始就碰回”改成开盘第一个小时(09:30–10:30)就碰回。 8. 打印周线和日线各自的:缺口数、当期碰回的比例、一开始就碰回的比例、向上和向下缺口分别的碰回率,以及每一档的缺口数和碰回率。 请每一步加中文注释,说明为什么这样做。
# 缺口回补:上周(昨天)的收盘价,这周(今天)有没有被碰回来
# 输入:清洗后的 ES 1 分钟 K 线(Databento ES.c.0,时间是 UTC,含 instrument_id 列)
import numpy as np
import pandas as pd
from pandas.tseries.holiday import (
AbstractHolidayCalendar, Holiday, GoodFriday, USMartinLutherKingJr, USPresidentsDay,
USMemorialDay, USLaborDay, USThanksgivingDay, nearest_workday, sunday_to_monday,
)
class NYSE(AbstractHolidayCalendar): # 纽交所全天休市日
rules = [
Holiday("元旦", month=1, day=1, observance=sunday_to_monday),
USMartinLutherKingJr, USPresidentsDay, GoodFriday, USMemorialDay,
Holiday("六月节", month=6, day=19, start_date="2022-01-01", observance=nearest_workday),
Holiday("独立日", month=7, day=4, observance=nearest_workday),
USLaborDay, USThanksgivingDay,
Holiday("圣诞节", month=12, day=25, observance=nearest_workday),
]
CLOSED = pd.to_datetime(["2012-10-29", "2012-10-30", "2018-12-05", "2025-01-09"]) # 临时休市
EDGES = [0, 5, 10, 15, 20, 30, 50, 100, np.inf] # 缺口大小分档(% 平均波幅)
df = pd.read_csv("ES-1min_标准版.csv", usecols=["ts_event", "instrument_id", "open", "high", "low", "close"])
t = pd.to_datetime(df.ts_event, utc=True).dt.tz_convert("US/Eastern").dt.tz_localize(None)
hm = t.dt.hour * 100 + t.dt.minute
rth = df[(hm >= 930) & (hm < 1615)].assign(date=t.dt.normalize(), h1=hm < 1030) # 日盘 09:30–16:15
g = rth.groupby("date")
day = g.agg(o=("open", "first"), h=("high", "max"), l=("low", "min"), c=("close", "last"), inst=("instrument_id", "first"))
first_hour = rth[rth.h1].groupby("date").agg(h=("high", "max"), l=("low", "min")) # 开盘第一个小时 09:30–10:30
def report(name, gaps):
gaps["档"] = pd.cut(gaps["大小"], EDGES, right=False)
print(f"== {name}:{len(gaps)} 个缺口,当期碰回 {100 * gaps['碰回'].mean():.1f}%,"
f"其中一开始就碰回 {100 * gaps['一开始'].mean():.1f}%")
by_dir = gaps.groupby("向上")["碰回"].mean() * 100
print(f"向上缺口 {by_dir[True]:.1f}%,向下缺口 {by_dir[False]:.1f}%")
print(gaps.groupby("档", observed=True).agg(缺口=("碰回", "size"), 碰回=("碰回", "mean"), 一开始=("一开始", "mean")).round(3).to_string())
# —— 周线:这周开盘价 vs 上周收盘价 ——
day["wk"] = day.index - pd.to_timedelta(day.index.dayofweek, unit="D") # 这周的周一
weeks = []
for wk, d in day.groupby("wk"):
d = d[d.inst == d.inst.iloc[0]] # 一周中间换了合约的话,换约以后的几天不看
weeks.append(dict(wk=wk, d=d, o=d.o.iloc[0], c=d.c.iloc[-1], rng=d.h.max() - d.l.min(),
i0=d.inst.iloc[0], i1=d.inst.iloc[-1]))
rows = []
for i in range(8, len(weeks)):
a, b = weeks[i - 1], weeks[i]
if (b["wk"] - a["wk"]).days != 7 or a["i1"] != b["i0"]:
continue # 上周缺失,或者两周不是同一个合约
gap = b["o"] - a["c"]
if gap == 0:
continue
abr = np.mean([w["rng"] for w in weeks[i - 8:i]]) # 过去 8 周平均每周的波幅
hit = (b["d"].l <= a["c"]) if gap > 0 else (b["d"].h >= a["c"]) # 哪几天碰到了上周收盘价
rows.append({"向上": gap > 0, "大小": abs(gap) / abr * 100, "碰回": hit.any(), "一开始": hit.iloc[0]})
report("周线", pd.DataFrame(rows))
# —— 日线:今天开盘价 vs 昨天收盘价(昨天 = 上一个股市交易日)——
cal = pd.bdate_range(day.index.min() - pd.Timedelta(days=10), day.index.max())
cal = cal[~cal.isin(NYSE().holidays(cal.min(), cal.max()).union(CLOSED))]
prev = pd.Series(cal[:-1], index=cal[1:]).reindex(day.index)
adr = (day.h - day.l).rolling(8).mean().shift(1) # 过去 8 天平均每天的波幅
rows = []
for i in range(8, len(day)):
today, yday = day.iloc[i], day.iloc[i - 1]
if prev.iloc[i] != day.index[i - 1] or today.inst != yday.inst:
continue # 昨天被清洗掉了,或者两天不是同一个合约
gap = today.o - yday.c
if gap == 0:
continue
fh = first_hour.loc[day.index[i]]
up = gap > 0
rows.append({"向上": up, "大小": abs(gap) / adr.iloc[i] * 100,
"碰回": today.l <= yday.c if up else today.h >= yday.c,
"一开始": fh.l <= yday.c if up else fh.h >= yday.c})
report("日线", pd.DataFrame(rows))局限
- 只看碰没碰到,不看碰到以后怎样:碰回上周收盘价,不代表在那里会反转,也可能直接穿过去。
- 周线样本小:只有 768 个缺口,按年份或按大小拆开以后更少。
- 半日市被删掉了:清洗时删掉了感恩节后的周五这类半日市,所以那一周的”上周收盘价”用的是周三的收盘,而不是周五半天的收盘。
- 换合约的那一周跳过了:数据是没有复权的连续合约,换约前后两周的价格对不上,所以每季度少一周。
- 没有看假期前后:比如长周末之后的缺口,可能和普通周末不一样,这里没有单独分开。