一句话结论:ES 日盘 5 分钟图上,99.97% 的日子会碰到 20 均线。16 年 3857 天里只有 2024-01-08 一天没碰到,Al Brooks 在直播里说的也是这一天。
但不一定马上碰到:第一根就碰到的只有 17.3%,到 10:30 是 60.6%,到 11:30 是 81.7%。
作用:开盘离均线很远时,可以确定今天迟早会回到均线,但可能要等两三个小时;它适合当”今天会到”的目标,不适合当”马上反转”的理由。
“价格每天都会回到均线”是交易里常听到的一句话,Al Brooks 在直播里也经常说。我用 ES 2010–2026 年的 5 分钟图检验了三件事:多少日子会碰到 20 均线?第一次碰到是在什么时候?价格离开均线以后,能连续多少根不碰?
2010–2026 年 · 日盘碰到 20 均线的日子
99.97%
样本 3857 天 · 没碰到的:2024-01-08
第一次碰到均线,是在第几根横轴是日盘第几根 5 分钟 K 线收线的时间,纵轴是到这根为止已经碰过均线的日子占比。
离开均线以后,能连续多少根不碰离开 = 整根 K 线在均线上方(最低价 > 均线)或下方(最高价 < 均线),连续几根算一段,每天开盘重新数。纵轴是至少连续这么多根的段,占所有段的比例,用对数刻度:每往下一格,比例小 10 倍。 按天算的话,56.9% 的日子出现过至少一段 20 根以上。
20 均线 = 5 分钟收盘价的 20 周期指数均线,只用日盘 K 线,一天接着一天算。碰到 = 均线在这根 K 线的最高价和最低价之间。离开均线那张图的比例是按"段"算的,不是按"天"算的:所选年份里,在均线上方一共 18719 段、下方 16191 段,平均每天大约 9.1 段;提示框里的百分比,是上方(或下方)所有段里,至少连续这么多根的段占多少。鼠标移到图上(手机上点一下)看具体数字。
16 年只有 1 天没碰到
- 3857 个交易日里,3856 天在日盘的某个时候碰到过 20 均线,比例是 99.97%。
- 唯一没碰到的是 2024-01-08:那天是一根很强的阳线日,从开盘涨到收盘,每一根 5 分钟 K 线的最低价都在均线上方。
- 除了 2024 年,其他每一年都是 100%。
Al Brooks 也说过这一天
我在 Al Brooks 2024 年的直播转录稿里找到了两段:
- 2024-03-21:他说 99.9%,甚至 99.99% 的日子都会在某个时候碰到均线;他印象里只有一天是整天都在均线上方、一次都没碰到,“大概是一两个月前”。
- 2024-01-09:开场就说前一天很特别,是他交易 37 年来第一次看到市场一整天都没碰到均线。
两段说的都是 2024 年 1 月 8 日。数据里 16 年也确实只有这一天,99.97% 正好落在他说的 99.9% 到 99.99% 之间,Al 的说法和数据对得上。
什么时候第一次碰到:不一定在开盘
| 到第几根 | 时间 | 已经碰过 |
|---|---|---|
| 第 1 根 | 9:35 | 17.3% |
| 第 6 根 | 10:00 | 40.5% |
| 第 12 根 | 10:30 | 60.6% |
| 第 18 根 | 11:00 | 72.7% |
| 第 24 根 | 11:30 | 81.7% |
| 第 36 根 | 12:30 | 91.6% |
很多日子开盘就在均线附近,第一根就碰到很正常。那会不会大部分”碰到”其实都是第一根?不会:第一根只占 17% 左右,八成多的日子是开盘离开均线以后,又回来碰的。
但回来的时间差别很大:到 10:30 还有四成日子没碰过,到中午还有一成。所以”今天一定会碰到均线”是对的,“开盘就会回到均线”就不对了。
离开均线:一段一般只有 3 根
整根 K 线都在均线上方(最低价高于均线)或下方(最高价低于均线),就算”离开均线”;连续几根算一段。
段是这样数的:每天从第 1 根 5 分钟 K 线往后看,连续几根都在均线上方,就是上方的一段;碰到均线或者跑到另一边,这一段就结束,之后再回到上方,算新的一段。只有 1 根在上方、下一根就碰回均线,也算一段(长度 1)。收盘时还没结束的,就在收盘处截断,第二天开盘重新数。下方的段也一样。
比如某一天是这样(示意,不是真实数据):
| 第几根 | 和均线的关系 | 算成 |
|---|---|---|
| 1–3 | 碰到 | — |
| 4–9 | 上方 | 上方第 1 段(6 根) |
| 10 | 碰到 | — |
| 11–12 | 上方 | 上方第 2 段(2 根) |
| 13–20 | 下方 | 下方第 1 段(8 根) |
| 21 | 碰到 | — |
| 22 起 | 上方 | 上方第 3 段 |
这一天上方有 3 段,下方有 1 段。把 3857 天每天的段数加起来,就是下面的总数:
- 上方段数:18719 段,平均每天约 4.9 段。
- 下方段数:16191 段,平均每天约 4.2 段。
- 合计 34910 段,平均每天约 9 段。图里的百分比都是按这些段来算的,比如”至少连续 10 根、上方 21.9%“,意思是上方的 18719 段里,有 21.9%(4103 段)至少连续了 10 根。
再看这些段有多长:
- 长度的中位数是 3 根。
- 93.5% 的段在 20 根以内。
- 在均线上方的段更长:连续 20 根以上的,上方占 8.3%,下方 5.9%;40 根以上的,上方 1.5%,下方 0.6%。ES 这 16 年大部分时间在涨,上涨趋势更能”挂”在均线上面。
连续 20 根以上不碰均线,单看一段是少数,但按天看并不少见:一天里有很多段,只要有一段够长就算,所以有 56.9% 的日子出现过至少一段 20 根以上。
Al Brooks 把这种情况叫 “20 gap bars”(20 根缺口 K 线):价格连续 20 根左右以上都没碰到均线,第一次回到均线时,通常会有人在均线附近买(下跌时是卖)。这篇只统计了它出现得多频繁,没有测试之后会怎么走,后面可以单独研究。
定义和复现
20 均线
- 数据:清洗后的 ES 1 分钟 K 线,2010-06 到 2026-09,清洗方法见这篇。
- 20 均线:日盘(美东 09:30–16:15)5 分钟 K 线收盘价的 20 周期指数均线,α = 2 / 21。只用日盘 K 线,一天接着一天往下算,不含夜盘。
- 没有成交的 5 分钟:2020 年 3 月熔断暂停交易时有 7 根 5 分钟 K 线没有成交,均线跳过它们、保持不变。
- 换合约:换合约后新合约的第一天,均线重新起算,这一天不计入(一共 66 天,加上数据第一天,样本从 3924 天变成 3857 天)。
- 碰到:最低价 ≤ 均线 ≤ 最高价。均线用这根 K 线收盘后的值,也就是图上画出来的那个值。
- 离开均线:最低价 > 均线(上方)或最高价 < 均线(下方)。
- 一段:连续几根都在均线上方(或都在下方),算一段;碰到均线或跑到另一边就结束。只有 1 根也算一段。每天开盘重新数,收盘时没结束的在收盘处截断。
- 上方段数 / 下方段数:把每天在均线上方(下方)的段数加起来。16 年里上方 18719 段、下方 16191 段,合计 34910 段,平均每天约 9 段。选不同年份时,图下面的说明会跟着变。
- 段的比例:图里”至少连续 L 根”的百分比 = 长度 ≥ L 的段数 ÷ 同一边的全部段数。它是按段算的,不是按天算的。
- 按天算:一天里只要有一段达到 20 根就算这一天”出现过”,这样的日子占 56.9%。
我有一份清洗过的 ES 期货 1 分钟 K 线 CSV(Databento 的 ES.c.0 连续合约,列有 ts_event、instrument_id、high、low、close,时间是 UTC)。请帮我写 Python 代码,统计“日盘 5 分钟图上,价格碰到 20 均线的情况”。规则如下: 1. 时间先转成美东时间(US/Eastern),自动处理夏令时。只看日盘 09:30–16:15,用 1 分钟 K 线合成 5 分钟 K 线,每天 81 根(09:30–09:35 是第 1 根)。某个 5 分钟里一笔成交都没有(比如 2020 年 3 月熔断暂停交易),这根就是空的。 2. 20 均线 = 5 分钟收盘价的 20 周期指数均线,α = 2 / 21:均线 = α × 这根的收盘价 + (1 − α) × 上一根的均线。只用日盘 K 线,一天接着一天往下算,不含夜盘。空的 K 线跳过,均线保持不变。 3. 换合约(instrument_id 变了)的那一天,均线从这天第一根的收盘价重新起算,这一天不计入统计,因为新旧合约价格对不上;数据的第一天同理。 4. 碰到:均线(用这根 K 线收盘后的值,也就是图上画的那个值)在这根 K 线的最高价和最低价之间,即 最低价 ≤ 均线 ≤ 最高价。 5. 打印:样本天数、当天碰到过均线的日子占比、第 1 根就碰到的占比、没碰到的是哪几天;第一次碰到在第 3、6、12、18、24、36 根以内的累计占比;再按年份打印每年碰到的比例。 6. 离开均线:整根 K 线在均线上方(最低价 > 均线)或下方(最高价 < 均线)。连续几根这样的 K 线算一段,每天开盘重新数。打印一共多少段、上方和下方各多少段、平均每天几段、长度的中位数、20 根以内的段占多少,上方和下方分别至少连续 10、20、40 根的段占多少(占同一边全部段数的比例),以及一天里出现过至少一段 20 根以上的日子占多少。 请每一步加中文注释,说明为什么这样做。
# 20 均线:日盘 5 分钟图上,有多少日子会碰到 20 均线?第一次碰到在第几根?离开均线能连续多少根?
# 输入:清洗后的 ES 1 分钟 K 线(Databento ES.c.0,时间是 UTC,含 instrument_id 列)
import numpy as np
import pandas as pd
df = pd.read_csv("ES-1min_标准版.csv", usecols=["ts_event", "instrument_id", "high", "low", "close"])
t = pd.to_datetime(df.ts_event, utc=True).dt.tz_convert("US/Eastern").dt.tz_localize(None)
m = t.dt.hour * 60 + t.dt.minute - 570 # 距 09:30 的分钟数
rth = df[(m >= 0) & (m < 405)].assign(date=t.dt.normalize(), bar=m // 5) # 日盘 09:30–16:15,每天 81 根 5 分钟
# 合成 5 分钟 K 线,排成“每天一行、81 列”的表;没有成交的 5 分钟(2020 年 3 月熔断)是空的
bars = rth.groupby(["date", "bar"]).agg(h=("high", "max"), l=("low", "min"), c=("close", "last"))
H, L, C = (bars[k].unstack().reindex(columns=range(81)).values for k in ["h", "l", "c"])
days = bars.index.get_level_values(0).unique()
inst = rth.groupby("date").instrument_id.first().values
# 20 均线:α = 2/21,只用日盘 K 线,一天接着一天往下算;空的 5 分钟跳过
# 换合约后的第一天重新起算,这一天不计入(新旧合约价格对不上);数据第一天同理
ema = np.full(H.shape, np.nan)
use = np.ones(len(days), bool)
e = None
for i in range(len(days)):
if i == 0 or inst[i] != inst[i - 1]:
e, use[i] = None, False
for k in range(81):
if not np.isnan(C[i, k]):
e = C[i, k] if e is None else 2 / 21 * C[i, k] + 19 / 21 * e
ema[i, k] = e
touch = (L <= ema) & (ema <= H) # 均线在这根 K 线的最高价和最低价之间
first = np.where(touch.any(1), touch.argmax(1) + 1, 0)[use] # 第一次碰到是第几根,没碰到记 0
d = days[use]
print(f"样本 {len(first)} 天,碰到 20 均线 {100 * (first > 0).mean():.2f}%,第 1 根就碰到 {100 * (first == 1).mean():.2f}%")
print("没碰到的日子:", list(d[first == 0].strftime("%Y-%m-%d")))
for n in [3, 6, 12, 18, 24, 36]:
print(f"到第 {n} 根已经碰过:{100 * ((first > 0) & (first <= n)).mean():.1f}%")
print((pd.Series(first > 0, index=d).groupby(d.year).mean() * 100).round(2).to_string()) # 每年碰到的比例
# 离开均线:整根 K 线在均线上方(最低价 > 均线)或下方(最高价 < 均线),连续几根算一段,每天开盘重新数
streaks, longest = {"上方": [], "下方": []}, []
for i in np.flatnonzero(use):
best = 0
for side, off in [("上方", L[i] > ema[i]), ("下方", H[i] < ema[i])]:
run = 0
for x in list(off) + [False]: # 末尾补一个 False,把收盘前的最后一段也收进来
if x:
run += 1
elif run:
streaks[side].append(run) # 一段结束,记下它的长度
best = max(best, run)
run = 0
longest.append(best)
up, down = np.array(streaks["上方"]), np.array(streaks["下方"])
both, longest = np.r_[up, down], np.array(longest)
print(f"离开均线 {len(both)} 段(上方 {len(up)} 段,下方 {len(down)} 段),平均每天 {len(both) / use.sum():.1f} 段")
print(f"长度的中位数 {np.median(both):.0f} 根,20 根以内 {100 * (both <= 20).mean():.1f}%")
for n in [10, 20, 40]:
print(f"至少连续 {n} 根:上方 {100 * (up >= n).mean():.1f}%({(up >= n).sum()} 段),下方 {100 * (down >= n).mean():.1f}%({(down >= n).sum()} 段)")
print(f"出现过至少一段 20 根以上的日子:{100 * (longest >= 20).mean():.1f}%")局限
- 只看碰没碰到,不看碰到以后怎样:碰到均线以后是反弹还是穿过去,这里没有统计。
- 均线只用日盘 K 线:如果在 24 小时图上算均线(包括夜盘),开盘时均线的位置不一样,第一根碰到的比例也会不一样。
- “一段”在收盘时截断:一段离开均线如果持续到收盘,就在收盘处结束,第二天开盘重新数,所以跨天的长趋势会被切成两段。