3. 流动性度量指标计算与分时段对比
上一节我们确认了盘口流动性在一天之内并非恒定,而是随开盘、午间、尾盘呈现明显的节律变化。但“感觉开盘流动性差、尾盘流动性好”只是定性判断,要真正用于策略过滤、执行成本估算和风控阈值设定,必须把流动性转化为可计算、可比较的数值。本节的核心任务,就是围绕买卖价差、有效价差、Amihud非流动性、订单簿深度与斜率这五类指标,完成从原始逐笔/快照数据到分时段统计结果的完整计算链路,并说明如何解读各时段之间的差异。
一、五类指标的定义与计算口径
在动手写代码之前,必须先固定每个指标的计算口径,否则不同时段、不同标的之间的数值无法横向比较。以下约定均以“买一价低于卖一价、价差为正”为参照,所有价格使用同一复权口径。
买卖价差(Quoted Spread)衡量盘口最优报价之间的绝对与相对宽度。绝对价差为 ask1 - bid1,相对价差为 (ask1 - bid1) / mid,其中 mid = (ask1 + bid1) / 2。相对价差消除了股价量级差异,是跨标的对比的首选。若某快照买一或卖一缺失(涨跌停封板),该样本应标记为无效而非按零处理。
有效价差(Effective Spread)从成交侧度量实际支付的交易成本,公式为 2 × |P_trade - mid_t|,其中 mid_t 取成交前最近一笔快照的中价。乘 2 是为了与报价价差在同一量纲上对比:若成交恰好打在卖一价,有效价差约等于报价价差;若成交在盘口中部甚至更优价位,有效价差会小于报价价差。成交方向可用成交价与中价比较判定,P_trade > mid 记为买方发起。
Amihud非流动性刻画“单位成交额推动价格变动的幅度”,公式为 |R_t| / Amount_t,其中 R_t 为区间收益率,Amount_t 为区间成交额。数值越大表示流动性越差。由于单笔比值噪声极大,实务中按时间桶(如 1 分钟或 5 分钟)先聚合再取均值,并对成交额过小的桶设下限过滤,避免除零放大。
订单簿深度衡量价格承受冲击的能力,常用口径为买侧与卖侧前 N 档(如 5 档或 10 档)挂单量之和,或进一步折算为金额。为便于跨时段比较,通常同时记录买侧深度、卖侧深度及其不对称度 (BidDepth - AskDepth) / (BidDepth + AskDepth)。
订单簿斜率反映挂单随价格偏离中价的衰减速度。一种可操作的定义是:对前 N 档的“累计挂单量”与“相对中价的价距”做线性回归,取斜率绝对值;斜率越陡,说明挂单越集中在中价附近,单位价距可吸收的量越小,流动性越脆弱。也可用简化式 Depth_N / (P_N - mid) 作为近似。
二、分时段统计的实现流程
指标算完后,需要按交易时段分组。A股连续竞价可划分为:开盘冲击段(09:30–10:00)、上午稳定段(10:00–11:30)、午后重启段(13:00–13:30)、下午稳定段(13:30–14:30)、尾盘段(14:30–15:00)。集合竞价时段单独标记,不混入连续竞价统计。
实现上建议采用“先逐快照/逐笔计算,再按时间桶聚合,最后按时段汇总”的三层结构。下面给出一个可直接改造的 Python 骨架,使用 pandas 完成分组:
import pandas as pd, numpy as np
def session_label(ts):
t = ts.time()
if t < pd.Timestamp("09:30").time(): return "集合竞价"
if t < pd.Timestamp("10:00").time(): return "开盘冲击"
if t < pd.Timestamp("11:30").time(): return "上午稳定"
if t < pd.Timestamp("13:30").time(): return "午后重启"
if t < pd.Timestamp("14:30").time(): return "下午稳定"
return "尾盘"
# snap: 含 timestamp, bid1, ask1, bid_depth5, ask_depth5
snap["mid"] = (snap.bid1 + snap.ask1) / 2
snap["rel_spread"] = (snap.ask1 - snap.bid1) / snap.mid
snap["depth"] = snap.bid_depth5 + snap.ask_depth5
snap["slope"] = snap.depth / ((snap.ask1 - snap.mid).clip(lower=1e-6))
snap["session"] = snap.timestamp.map(session_label)
# trade: 含 timestamp, price, amount
trade["mid_t"] = trade.timestamp.map(
lambda x: snap.loc[snap.timestamp <= x, "mid"].iloc[-1])
trade["eff_spread"] = 2 * (trade.price - trade.mid_t).abs() / trade.mid_t
trade["session"] = trade.timestamp.map(session_label)
summary = snap.groupby("session").agg(
rel_spread=("rel_spread", "mean"),
depth=("depth", "mean"),
slope=("slope", "mean"))
summary["eff_spread"] = trade.groupby("session")["eff_spread"].mean()
Amihud 指标需先按 1 分钟桶聚合收益率与成交额,再按时段取均值。注意所有均值都应同时输出中位数与样本数,因为开盘段样本少且分布偏斜,单看均值容易被极端值带偏。
三、结果解读与差异识别
上图以归一化方式呈现了典型形态:开盘冲击段的相对价差与有效价差同时处于高位,深度最低、斜率最陡,说明此时盘口既宽又薄,单位价距可吸收的挂单量少,大单冲击成本最高;上午稳定段价差回落、深度抬升,斜率变缓,流动性明显改善;午后重启段出现一次小幅反弹,价差与斜率再度走阔,但幅度弱于开盘;下午稳定段各项指标趋于平稳,是全天流动性较优的窗口;尾盘段价差略有回升而深度仍维持较高水平,斜率继续变缓,呈现“价差略宽但承接力强”的特征。
解读时要注意三点。第一,价差类指标与深度类指标方向相反:价差越小、深度越大、斜率越缓,流动性越好,因此不能把四条曲线简单相加成一个“流动性分数”而不加说明。第二,有效价差与相对价差的差距本身包含信息:若某时段有效价差显著低于报价价差,说明成交大量发生在盘口中部,隐藏流动性较好;若两者接近,说明成交主要吃盘口,成本更接近报价。第三,Amihud 指标应与价差指标交叉验证,若某时段价差正常但 Amihud 异常偏高,往往意味着成交额萎缩导致单位冲击放大,属于“表面平静、实际脆弱”的状态。
四、从指标到可用结论
完成分时段对比后,应形成可执行的判断,而不是停留在“开盘差、尾盘好”的描述。例如:将开盘冲击段的相对价差中位数作为执行算法的“宽价差保护阈值”,超过该值则拆单放缓;将午后重启段的斜率作为挂单集中度的监控指标,斜率突然变陡时提示盘口变薄;将 Amihud 的分时段均值作为大单冲击成本的预估系数,用于事前成本估算。指标的价值在于被嵌入决策,而非仅用于展示。
最后提醒口径一致性:所有时段必须使用同一复权方式、同一档位数量、同一时间桶长度;集合竞价与连续竞价分开统计;涨跌停封板快照单独标记。只有口径统一,分时段差异才反映真实的流动性节律,而不是计算方式变化带来的假象。