4. 委买委卖总量失衡事件识别
指标构造:十档委买委卖总量比
本节任务的核心对象是十档委买总量与十档委卖总量的比值。与例3只看买一卖一量比不同,这里把买盘和卖盘各十档的挂单量全部累加,得到更稳定的盘口深度对比。定义如下(以快照时刻 t 为单位):
BidVol10(t) = Σ bid_size[i],i 从 1 到 10,即买一到买十档挂单量之和;AskVol10(t) = Σ ask_size[i],i 从 1 到 10,即卖一到卖十档挂单量之和;IMB(t) = BidVol10(t) / AskVol10(t),即委买委卖总量比(Imbalance Ratio)。
约定:IMB > 1 表示买盘挂单总量占优,IMB < 1 表示卖盘占优。为避免除零,当 AskVol10 = 0 时该快照标记为无效并跳过。单点失衡容易受大单挂撤干扰,因此判定规则必须叠加持续时长约束:只有失衡状态连续维持达到 min_duration 个快照(或秒数)才计为一次事件。
失衡判定规则设计
规则分三层:阈值层、持续层、事件层。阈值层给出方向与强度,持续层过滤瞬时噪声,事件层输出起止时间与峰值。下面给出可直接运行的 Python 实现,输入为包含十档买卖挂单量的分钟级(或秒级)快照 DataFrame。
import pandas as pd
import numpy as np
def build_imbalance(snap: pd.DataFrame) -> pd.DataFrame:
"""snap 需含 bid1..bid10 / ask1..ask10 的挂单量列,以及时间列 ts。"""
bid_cols = [f"bid{i}" for i in range(1, 11)]
ask_cols = [f"ask{i}" for i in range(1, 11)]
df = snap.copy()
df["bid_vol10"] = df[bid_cols].sum(axis=1)
df["ask_vol10"] = df[ask_cols].sum(axis=1)
# 卖盘为 0 时比值无意义,置为 NaN
df["imb"] = np.where(df["ask_vol10"] > 0,
df["bid_vol10"] / df["ask_vol10"], np.nan)
return df
def detect_imbalance_events(df: pd.DataFrame,
upper: float = 2.0,
lower: float = 0.5,
min_duration: int = 3) -> pd.DataFrame:
"""upper/lower 为失衡阈值,min_duration 为连续快照数下限。"""
d = df.dropna(subset=["imb"]).reset_index(drop=True)
# 状态:1 买盘严重失衡,-1 卖盘严重失衡,0 正常
d["state"] = 0
d.loc[d["imb"] >= upper, "state"] = 1
d.loc[d["imb"] <= lower, "state"] = -1
events = []
start = None
cur_state = 0
for i, s in enumerate(d["state"]):
if s != cur_state:
# 结束上一段
if cur_state != 0 and start is not None:
length = i - start
if length >= min_duration:
seg = d.iloc[start:i]
events.append({
"start_ts": seg["ts"].iloc[0],
"end_ts": seg["ts"].iloc[-1],
"direction": "买盘失衡" if cur_state == 1 else "卖盘失衡",
"duration": length,
"peak_imb": seg["imb"].max() if cur_state == 1 else seg["imb"].min(),
})
start = i if s != 0 else None
cur_state = s
# 收尾
if cur_state != 0 and start is not None:
length = len(d) - start
if length >= min_duration:
seg = d.iloc[start:]
events.append({
"start_ts": seg["ts"].iloc[0],
"end_ts": seg["ts"].iloc[-1],
"direction": "买盘失衡" if cur_state == 1 else "卖盘失衡",
"duration": length,
"peak_imb": seg["imb"].max() if cur_state == 1 else seg["imb"].min(),
})
return pd.DataFrame(events)
参数选择建议:upper=2.0、lower=0.5 对应买卖总量相差一倍,属于较明显的失衡;min_duration=3 在分钟级快照下约等于持续 3 分钟,可过滤单笔大单造成的瞬时跳变。若数据为秒级,应把 min_duration 换算为对应秒数,例如 60 秒。
失衡事件与后续价格波动的对应关系
识别出事件后,需要统计事件发生后的价格变化,验证失衡是否具有方向性信息。做法是:对每个事件,取事件结束时刻的中间价 mid_end,再取事件结束后第 N 个快照的中间价 mid_future,计算收益率 ret = mid_future / mid_end - 1,并按方向分组统计。
def attach_forward_return(df, events, horizon=5):
d = df.dropna(subset=["imb"]).reset_index(drop=True)
d["mid"] = (d["bid1_price"] + d["ask1_price"]) / 2
rows = []
for _, ev in events.iterrows():
idx = d.index[d["ts"] == ev["end_ts"]]
if len(idx) == 0:
continue
i = idx[0]
j = min(i + horizon, len(d) - 1)
ret = d["mid"].iloc[j] / d["mid"].iloc[i] - 1
rows.append({**ev.to_dict(), "fwd_ret": ret})
return pd.DataFrame(rows)
# 示例调用
# snap = pd.read_csv("snapshot.csv", parse_dates=["ts"])
# df = build_imbalance(snap)
# ev = detect_imbalance_events(df, upper=2.0, lower=0.5, min_duration=3)
# res = attach_forward_return(df, ev, horizon=5)
# print(res.groupby("direction")["fwd_ret"].agg(["count", "mean", "std"]))
判读逻辑:若“买盘失衡”事件后的平均收益显著为正、且“卖盘失衡”事件后平均收益显著为负,说明该指标具有方向性预测价值;若两组均值接近 0 或符号相反,则说明当前阈值下失衡事件更多是噪声或已被市场消化。示例数据下的预期现象应明确标注为示例,实际结论需用真实快照验证。
结果判读与常见陷阱
- 阈值不能照搬:不同标的的十档深度差异很大,
upper/lower应结合该标的 IMB 的历史分位数确定,例如取 90% 与 10% 分位作为上下阈值。 - 持续时长与采样频率绑定:分钟级快照的
min_duration=3与秒级快照的 3 个点含义完全不同,必须换算成统一的时间长度再比较。 - 方向性验证不可省:识别出事件不等于有预测价值,必须用事件后收益分组统计来检验,避免把噪声当成信号。
- 大单挂撤干扰:十档总量比单档更稳,但仍可能被单笔巨额挂单拉动,可结合例6的挂撤痕迹做二次过滤。
build_imbalance 与 detect_imbalance_events,确认事件数量与人工观察的盘口异动大致吻合,再调整阈值与持续时长,最后用 attach_forward_return 做方向性检验。