12、订单簿特征工程:从订单簿提取特征、归一化、时序特征、截面特征
做量化交易这几年,我越来越觉得一句话是真理:特征决定上限,模型只是逼近这个上限。订单簿数据,说白了就是市场最原始的博弈记录——每一笔挂单、每一次撤单,都是真金白银投票的结果。但原始数据太糙了,没法直接用。你得把它「洗」成模型能看懂的特征。
这一章,我就带你走一遍订单簿特征工程的完整流程。从原始数据到归一化,从时序特征到截面特征,咱们一步步来。
12.1 从原始订单簿提取基础特征
原始订单簿长什么样?说白了就是一张大表,记录着每个价格档位的挂单量和挂单方向。但直接拿这些数字喂给模型,效果很差。为什么?因为价格本身不包含信息,真正有价值的是相对关系。
我个人习惯,第一步先算这几个基础特征:
- 加权平均价格(WAP):买一价和卖一价的加权平均,反映当前公允价格。公式很简单:
WAP = (bid_price * ask_size + ask_price * bid_size) / (bid_size + ask_size) - 订单簿失衡(Order Imbalance):衡量买卖双方的力量对比。
Imbalance = (bid_volume - ask_volume) / (bid_volume + ask_volume)。正值表示买方强势,负值表示卖方强势。 - 价差(Spread):卖一价减去买一价。价差越小,流动性越好。
- 深度(Depth):前N档的总挂单量。我一般取前5档,太深了噪声大。
# 基础特征提取示例(Python伪代码)
def extract_basic_features(orderbook):
bid_prices = orderbook['bid_prices']
ask_prices = orderbook['ask_prices']
bid_sizes = orderbook['bid_sizes']
ask_sizes = orderbook['ask_sizes']
# 加权平均价格
wap = (bid_prices[0] * ask_sizes[0] + ask_prices[0] * bid_sizes[0]) / (bid_sizes[0] + ask_sizes[0])
# 订单簿失衡
total_bid = sum(bid_sizes[:5])
total_ask = sum(ask_sizes[:5])
imbalance = (total_bid - total_ask) / (total_bid + total_ask)
# 价差
spread = ask_prices[0] - bid_prices[0]
# 深度
depth_bid = sum(bid_sizes[:5])
depth_ask = sum(ask_sizes[:5])
return {'wap': wap, 'imbalance': imbalance, 'spread': spread, 'depth_bid': depth_bid, 'depth_ask': depth_ask}
12.2 归一化:让特征站在同一起跑线
特征提取完了,你会发现一个问题:价差可能是0.01,而深度可能是10000。量纲差了好几个数量级。如果不做归一化,模型会天然地认为「深度」比「价差」重要——这显然是错的。
我常用的归一化方法有两种:
| 方法 | 公式 | 适用场景 |
|---|---|---|
| Z-score标准化 | x' = (x - μ) / σ |
特征分布近似正态,或存在异常值 |
| Min-Max归一化 | x' = (x - min) / (max - min) |
特征有明确边界,如比例类特征 |
对于订单簿特征,我个人更推荐Z-score。因为订单簿数据经常有极端行情(比如闪崩),Min-Max会被这些极端值拉偏。Z-score对异常值相对鲁棒一些。
12.3 时序特征:捕捉市场动态
单点的特征只能告诉你「现在怎么样」,但交易决策更需要知道「趋势怎么样」。这就是时序特征的价值。
我常用的时序特征包括:
- 滚动均值(Rolling Mean):过去N个时间步的WAP均值。比如5步均值、20步均值。反映短期和中期趋势。
- 差分(Difference):当前值减去前一个值。说白了就是变化量。WAP的差分就是价格变动。
- 波动率(Volatility):过去N步的WAP标准差。波动率越大,市场越不稳定。
- 动量(Momentum):当前WAP与N步前WAP的比值。大于1表示上涨,小于1表示下跌。
# 时序特征构建示例
def build_time_series_features(df, window=10):
df['wap_ma'] = df['wap'].rolling(window=window).mean()
df['wap_diff'] = df['wap'].diff()
df['wap_volatility'] = df['wap'].rolling(window=window).std()
df['momentum'] = df['wap'] / df['wap'].shift(window)
return df
嗯,这里要注意:滚动窗口的大小怎么选?没有标准答案。我个人习惯用5、10、20、50这几个窗口,分别对应秒级、分钟级、5分钟级和15分钟级的交易节奏。具体用哪个,得看你的交易频率。
12.4 截面特征:多档位信息融合
时序特征看的是「时间轴」,截面特征看的是「价格轴」。订单簿有多个档位,每个档位都藏着信息。截面特征就是把这些信息融合起来。
我常用的截面特征:
- 价格斜率(Price Slope):对前N档的价格做线性回归,斜率反映了价格随档位变化的趋势。斜率越大,说明深度越浅。
- 量能分布(Volume Distribution):前N档挂单量的分布特征,比如集中度、偏度。集中度高的订单簿,容易被大单击穿。
- 累积深度曲线(Cumulative Depth Curve):从买一/卖一开始,累积挂单量随价格偏移的变化。这个曲线能反映市场的「承压能力」。
- 多档位失衡(Multi-level Imbalance):不只是看买一卖一,而是看前N档的累积失衡。比如前3档失衡、前5档失衡。
# 截面特征构建示例
def build_cross_sectional_features(orderbook, levels=5):
bid_prices = orderbook['bid_prices'][:levels]
ask_prices = orderbook['ask_prices'][:levels]
bid_sizes = orderbook['bid_sizes'][:levels]
ask_sizes = orderbook['ask_sizes'][:levels]
# 价格斜率(对档位做线性回归)
x = np.arange(levels)
slope_bid = np.polyfit(x, bid_prices, 1)[0]
slope_ask = np.polyfit(x, ask_prices, 1)[0]
# 累积深度
cum_bid = np.cumsum(bid_sizes)
cum_ask = np.cumsum(ask_sizes)
# 多档位失衡
imbalance_3 = (sum(bid_sizes[:3]) - sum(ask_sizes[:3])) / (sum(bid_sizes[:3]) + sum(ask_sizes[:3]))
imbalance_5 = (sum(bid_sizes[:5]) - sum(ask_sizes[:5])) / (sum(bid_sizes[:5]) + sum(ask_sizes[:5]))
return {'slope_bid': slope_bid, 'slope_ask': slope_ask,
'cum_bid': cum_bid, 'cum_ask': cum_ask,
'imbalance_3': imbalance_3, 'imbalance_5': imbalance_5}
12.5 特征组合与筛选
特征造了一大堆,但并不是越多越好。我见过有人一口气造了200多个特征,结果模型训练时间翻了好几倍,效果反而下降了。这就是典型的「维度灾难」。
我的做法是:
- 先粗筛:计算每个特征与目标变量的相关性,去掉相关性低于0.05的特征。
- 再精筛:用随机森林或XGBoost的特征重要性排序,保留Top 30-50个特征。
- 最后做PCA:如果特征之间相关性太高(比如多个滚动均值),用PCA降维到20维左右。
好了,特征工程这块就聊到这儿。记住一句话:好的特征工程,胜过十个调参大师。下一章咱们会把这些特征喂进模型里,看看实际效果如何。