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