3. 尾段回落时间窗口定位:回落起点与低点的时间差统计
任务拆解:从分时序列中提取两个时刻
本节要解决的是一个“时间定位”问题:在某个竞价交易日的分时数据里,先找到尾段回落的起点时刻,再找到回落区间的最低点时刻,然后计算两者之间的时间差与期间价格斜率。时间差回答“跌得快不快”,斜率回答“跌得陡不陡”,两者结合才能区分急跌与缓跌。
为了让定位过程可复现,先明确三个约定:
- 尾段范围:以 14:30 为尾段起点,收盘 15:00 为终点,共 30 分钟。若当日有集合竞价收盘,14:57–15:00 单独标记,不参与斜率拟合。
- 起点时刻 t_start:尾段内瞬时价首次跌破“尾段起始价 × (1 − 0.3%)”的那一分钟,且此后不再回到该阈值上方,视为回落确认起点。
- 最低点时刻 t_low:从 t_start 到 15:00 之间瞬时价的最小值所在分钟;若最小值出现多次,取最早一次。
时间差 Δt = t_low − t_start(单位:分钟),斜率 k = (P_low − P_start) / Δt(单位:元/分钟,负值表示下行)。
示例数据与定位过程
下表是一段教学示例分时数据(1 分钟粒度,仅列尾段关键点),价格为假设值,用于演示定位与计算逻辑,不代表任何真实标的。
| 时刻 | 瞬时价(元) | 相对尾段起始价 | 状态标记 |
|---|---|---|---|
| 14:30 | 20.00 | 0.00% | 尾段起始 |
| 14:33 | 19.96 | −0.20% | 未破阈值 |
| 14:36 | 19.93 | −0.35% | t_start 候选 |
| 14:38 | 19.95 | −0.25% | 回抽,未确认 |
| 14:41 | 19.90 | −0.50% | t_start 确认 |
| 14:47 | 19.78 | −1.10% | 下行中 |
| 14:52 | 19.71 | −1.45% | 下行中 |
| 14:55 | 19.68 | −1.60% | t_low |
| 14:58 | 19.70 | −1.50% | 小幅回抽 |
| 15:00 | 19.69 | −1.55% | 收盘 |
按约定:14:36 首次跌破 −0.3% 阈值,但 14:38 回抽至 −0.25%,未满足“不再回到阈值上方”,因此起点顺延至 14:41 的确认跌破。最低点出现在 14:55,价格 19.68 元。
计算:Δt = 14:55 − 14:41 = 14 分钟;k = (19.68 − 19.90) / 14 ≈ −0.0157 元/分钟,即每分钟约下跌 1.57 分钱,折合相对斜率约 −0.079%/分钟。
可复用的定位与统计代码
下面的 Python 片段把上述约定固化为函数,输入为按时间升序的分时记录(含 time 与 price 两列),输出起点、低点、时间差与斜率。代码自洽可运行,示例数据内嵌,便于直接验证。
import pandas as pd
def locate_tail_pullback(df, tail_start="14:30", threshold=0.003):
"""定位尾段回落起点与低点,返回时间差与斜率。"""
df = df.copy()
df["time"] = pd.to_datetime(df["time"])
tail = df[df["time"].dt.strftime("%H:%M") >= tail_start].reset_index(drop=True)
if len(tail) < 3:
return None
p0 = tail.loc[0, "price"] # 尾段起始价
line = p0 * (1 - threshold) # 跌破阈值线
# 起点:首次跌破后不再回到阈值上方
t_start_idx = None
for i in range(len(tail)):
if tail.loc[i, "price"] < line:
if (tail.loc[i:, "price"] >= line).sum() == 0:
t_start_idx = i
break
if t_start_idx is None:
return None
seg = tail.loc[t_start_idx:].reset_index(drop=True)
low_idx = seg["price"].idxmin() # 最早出现的最小值
t_start = seg.loc[0, "time"]
t_low = seg.loc[low_idx, "time"]
p_start = seg.loc[0, "price"]
p_low = seg.loc[low_idx, "price"]
dt_min = (t_low - t_start).total_seconds() / 60
slope = (p_low - p_start) / dt_min if dt_min > 0 else float("nan")
return {
"t_start": t_start.strftime("%H:%M"),
"t_low": t_low.strftime("%H:%M"),
"dt_min": dt_min,
"p_start": p_start,
"p_low": p_low,
"slope_yuan_per_min": round(slope, 4),
"slope_pct_per_min": round(slope / p_start * 100, 4),
}
# 教学示例数据(假设值)
demo = pd.DataFrame({
"time": ["2024-06-03 14:30","2024-06-03 14:33","2024-06-03 14:36",
"2024-06-03 14:38","2024-06-03 14:41","2024-06-03 14:47",
"2024-06-03 14:52","2024-06-03 14:55","2024-06-03 14:58",
"2024-06-03 15:00"],
"price": [20.00, 19.96, 19.93, 19.95, 19.90, 19.78, 19.71, 19.68, 19.70, 19.69]
})
print(locate_tail_pullback(demo))
按示例数据运行,预期输出为:t_start=14:41、t_low=14:55、dt_min=14.0、slope_yuan_per_min≈-0.0157、slope_pct_per_min≈-0.0789。若实际输出与上述不一致,优先检查时间列是否被解析为字符串、阈值是否按小数传入(0.003 而非 0.3)。
批量统计与形态归类
单日定位只是第一步,本节目标还要求“统计”。把函数套到多日样本上,按 Δt 与 |k| 分箱,即可得到急跌/缓跌的分布。下面给出统计骨架,字段与判读口径和上文一致。
def classify(row, fast_dt=10, fast_k=0.02, slow_dt=20, slow_k=0.01):
if row is None:
return "无回落"
dt, k = row["dt_min"], abs(row["slope_yuan_per_min"])
if dt <= fast_dt and k >= fast_k:
return "急跌"
if dt >= slow_dt and k <= slow_k:
return "缓跌"
return "中性回落"
results = []
for day, g in all_days.groupby("date"):
r = locate_tail_pullback(g)
results.append({"date": day, "shape": classify(r), **(r or {})})
summary = pd.DataFrame(results)["shape"].value_counts(normalize=True)
print(summary)
结果判读与常见偏差
- 起点被“假跌破”带偏:若阈值过松(如 −0.1%),14:33 就会被误判为起点,Δt 被拉长、斜率被摊平,急跌会被误读成缓跌。示例中 14:36 的回抽正是需要排除的情形。
- 低点取到收盘集合竞价:14:57 后的价格波动机制不同,若把 15:00 收盘价当作 t_low,Δt 会系统性偏大。建议在尾段内单独切出 14:57–15:00 并标注。
- 斜率单位混用:元/分钟与 %/分钟不可直接比较。跨标的统计时统一用 %/分钟,单标的复盘时元/分钟更直观。
- Δt 为 0 的边界:若起点当分钟即为最低点,斜率为无穷大,代码中返回 NaN,应单独归为“瞬时跳水”而非急跌。
完成上述定位与统计后,本节的核心产出是一张按日排列的 t_start / t_low / Δt / k 明细表,以及基于 Δt 与 |k| 的形态分布。它直接为后续的量能配合分析与高位/低位分类提供时间锚点。