4. 特征工程(上):基于订单簿的特征、基于成交的特征、时间序列特征

各位同学,欢迎来到特征工程的上半场。

说实话,很多做量化的人,模型跑不出来,第一反应是算法不行。但我个人经验告诉我——90%的问题出在特征上。你喂给模型的是垃圾,它吐出来的只能是更精致的垃圾。

这一章,我们聚焦在流动性预判最核心的三个特征维度:订单簿特征、成交特征、时间序列特征。我会把我在实盘项目中踩过的坑、总结的经验,一股脑倒给你们。

核心观点:流动性特征不是越多越好,而是越「对」越好。你想想看,一个订单簿深度特征,在不同市场微观结构下,含义可能完全相反。

流动性预判特征 订单簿特征 价差 | 深度 | 斜率 买卖压力比 成交特征 成交量 | 成交额 | 笔数 平均每笔成交额 时间序列特征 滚动窗口统计量 均值 | 标准差 | 分位数 图:流动性预判三大特征维度

4.1 基于订单簿的特征

订单簿,说白了就是市场挂单的「实时快照」。我个人习惯把订单簿特征分成三类:价差、深度、斜率。这三兄弟,每一个都能从不同角度告诉你流动性好不好。

4.1.1 价差(Spread)

价差是最直观的流动性指标。价差越小,说明买卖双方越愿意「贴身肉搏」,流动性越好。

常用的价差指标有两个:

  • 绝对价差 = 卖一价 - 买一价
  • 相对价差 = (卖一价 - 买一价) / 中间价

嗯,这里要注意:绝对价差在高价股和低价股之间没法直接比较。所以我一般用相对价差做跨品种对比。

我的经验:在A股市场,相对价差超过0.5%时,我基本不会考虑做高频策略。流动性太差了,一进去就被滑点吃掉利润。

4.1.2 深度(Depth)

深度衡量的是「在某个价格水平上,你能吃掉多少单子」。我常用的是前N档挂单总量

举个例子,买一到买五的总挂单量,就是买方深度。卖一到卖五的总挂单量,就是卖方深度。

# 计算前5档买卖深度
buy_depth = df['bid_size_1'] + df['bid_size_2'] + df['bid_size_3'] + df['bid_size_4'] + df['bid_size_5']
sell_depth = df['ask_size_1'] + df['ask_size_2'] + df['ask_size_3'] + df['ask_size_4'] + df['ask_size_5']

# 买卖深度比
depth_ratio = buy_depth / (sell_depth + 1e-8)  # 防止除零

我曾经在回测中发现,买卖深度比突然飙升到2以上,往往预示着大资金在托盘。但别高兴太早——有时候这是主力在「挂假单」,等你冲进去他就撤单了。

避坑指南:我曾经被「挂单-撤单」模式坑过。后来我加了一个特征:挂单存活时间。如果某档挂单在1秒内被撤掉超过3次,我就认为这是「虚假深度」,直接过滤掉。

4.1.3 斜率(Slope)

斜率描述的是订单簿的「陡峭程度」。你想想看,如果订单簿很平缓,说明各个价位都有充足的挂单,流动性好。如果很陡峭,说明流动性集中在少数价位。

计算方式其实很简单:

# 计算卖单斜率(用前5档)
prices = [df[f'ask_price_{i}'] for i in range(1, 6)]
sizes = [df[f'ask_size_{i}'] for i in range(1, 6)]

# 用线性回归拟合价格-挂单量关系
from scipy import stats
slope, intercept, r_value, p_value, std_err = stats.linregress(prices, sizes)

我个人习惯用对数斜率,因为挂单量分布往往呈指数形态。对数斜率更稳定,不容易被极端值带偏。

4.2 基于成交的特征

订单簿是「静态快照」,成交数据是「动态行为」。两者结合,才能看清流动性全貌。

4.2.1 成交量与成交额

这两个指标太基础了,但基础不代表简单。我见过很多人直接用原始成交量,结果模型被「放量」和「缩量」的绝对数值搞晕了。

我的做法是:做标准化。用过去N天的平均成交量做分母,得到「相对成交量」。

# 相对成交量
df['rel_volume'] = df['volume'] / df['volume'].rolling(20).mean()
df['rel_amount'] = df['amount'] / df['amount'].rolling(20).mean()

为什么要这么做?因为一只股票平时成交1亿,今天成交2亿,这叫放量。但另一只股票平时成交100亿,今天成交2亿,这叫缩量。绝对数值没有可比性。

4.2.2 笔数

笔数这个特征,很多人忽略。但我告诉你,它非常有用。

同样的成交量,如果笔数很多,说明是散户在「蚂蚁搬家」,流动性分散。如果笔数很少,说明是机构在「大象过河」,流动性集中。

我常用一个衍生特征:平均每笔成交额

df['avg_trade_size'] = df['amount'] / (df['trade_count'] + 1e-8)

平均每笔成交额突然变大,往往意味着机构在行动。我在实盘中用这个特征,成功捕捉过好几次大单进场。

关键洞察:平均每笔成交额 + 买卖深度比,这两个特征组合在一起,能有效识别「真假大单」。如果深度比高但每笔成交额低,大概率是挂假单。反之,如果每笔成交额高但深度比正常,说明是真金白银在买。

4.3 时间序列特征

单点的特征不够,我们需要看变化趋势。时间序列特征,说白了就是给原始特征加上「记忆」。

4.3.1 滚动窗口统计量

我最常用的滚动窗口统计量包括:

  • 滚动均值:平滑噪声,看趋势
  • 滚动标准差:衡量波动性
  • 滚动分位数:看当前值在历史中的位置
  • 滚动最大值/最小值:捕捉极端情况
# 以价差为例,计算滚动统计量
window = 20  # 20个时间窗口

df['spread_mean'] = df['spread'].rolling(window).mean()
df['spread_std'] = df['spread'].rolling(window).std()
df['spread_zscore'] = (df['spread'] - df['spread_mean']) / df['spread_std']
df['spread_quantile_90'] = df['spread'].rolling(window).quantile(0.9)

嗯,这里有个坑:窗口大小的选择。窗口太小,噪声大;窗口太大,反应迟钝。

我个人习惯的做法是:多窗口并行。比如同时计算5分钟、30分钟、2小时的滚动统计量,让模型自己去学哪个窗口更有用。

我的小技巧:滚动Z-score比滚动均值更有信息量。Z-score告诉你「当前值偏离正常水平多少个标准差」,这在流动性突变时非常敏感。我曾经用Z-score > 3作为预警信号,提前识别了多次流动性枯竭事件。

4.3.2 差分与变化率

除了滚动统计量,我还喜欢用一阶差分变化率。它们能捕捉特征的「加速度」。

# 价差的变化率
df['spread_pct_change'] = df['spread'].pct_change()

# 深度的差分
df['depth_diff'] = df['total_depth'].diff()

为什么变化率重要?因为流动性恶化往往不是线性的。价差从0.1%涨到0.2%,可能只是正常波动。但如果在10秒内从0.1%涨到0.5%,那就是流动性在「崩塌」。

避坑指南:我曾经在计算变化率时,没有处理「前值为0」的情况。结果pct_change返回了inf,模型直接崩了。后来我加了一个小处理:如果前值为0,变化率设为NaN或者一个很大的数(比如100%)。

4.4 特征组合的实战思路

单个特征就像单个乐器,组合起来才能奏出交响乐。我分享一个我常用的特征组合思路:

特征组合 含义 应用场景
价差 + 深度 价差小 + 深度大 = 优质流动性 判断是否适合高频交易
斜率 + 成交量 斜率陡 + 成交量放大 = 大单在冲击 识别大单进场/离场
平均每笔成交额 + 深度比 每笔大 + 深度比异常 = 机构行为 跟踪聪明钱
滚动Z-score + 变化率 Z-score高 + 变化率加速 = 流动性危机 风险预警

你想想看,这些组合其实都是在回答同一个问题:现在的流动性状态,是正常的、在改善的、还是在恶化的?

好了,这一章的内容就到这里。特征工程的下半场,我们会继续聊另类数据特征、文本特征、以及特征筛选的方法。到时候见。


公众号:蓝海数据掘金营,微信deep3321