4. 特征工程(下):订单簿斜率、订单簿不平衡度、微观结构特征(VPIN、买卖压力)
好,咱们接着聊特征工程。上一节我们把基础特征撸了一遍,这一节要上点硬菜了——订单簿斜率、不平衡度,还有微观结构里的VPIN和买卖压力。这些特征,说白了就是帮你从订单簿的“表面波动”里,挖出更深层的市场博弈信息。
我个人习惯,做高频策略时,斜率和不平衡度是必选项。它们就像市场的“心电图”,能告诉你当前是买方在发力,还是卖方在砸盘。而VPIN这类微观结构特征,更像是一台“CT机”,能扫描出订单流里的毒瘤——比如老鼠仓或者信息泄露。
4.1 订单簿斜率:市场的“坡度”
订单簿斜率,衡量的是价格深度关系的陡峭程度。你可以想象成爬山——坡度越陡,说明往上走(买)或往下走(卖)的阻力越大。
怎么算? 我一般用线性回归拟合买卖两侧的价量关系。具体来说,取前N档的累计深度和对应价格,拟合一条直线,斜率就是我们要的值。
def order_book_slope(df, levels=5):
"""
计算订单簿斜率
df: 包含 bid_price, bid_size, ask_price, ask_size 的DataFrame
"""
# 买方斜率
bid_prices = [f'bid_price_{i}' for i in range(1, levels+1)]
bid_sizes = [f'bid_size_{i}' for i in range(1, levels+1)]
# 用累计深度作为x,价格作为y
x_bid = df[bid_sizes].cumsum(axis=1).values
y_bid = df[bid_prices].values
# 简单线性回归,返回斜率
slope_bid = np.polyfit(x_bid, y_bid, deg=1)[0]
# 卖方同理
ask_prices = [f'ask_price_{i}' for i in range(1, levels+1)]
ask_sizes = [f'ask_size_{i}' for i in range(1, levels+1)]
x_ask = df[ask_sizes].cumsum(axis=1).values
y_ask = df[ask_prices].values
slope_ask = np.polyfit(x_ask, y_ask, deg=1)[0]
return slope_bid, slope_ask
4.2 订单簿不平衡度:谁在主导?
不平衡度,就是看买卖双方谁更“用力”。公式很简单:(买方总深度 - 卖方总深度) / (买方总深度 + 卖方总深度)。值域在[-1, 1],正数表示买方占优,负数表示卖方占优。
但这里有个坑——直接用原始深度算,容易被大单刷屏。你想想看,一个挂单1000手的幽灵单,能瞬间把不平衡度拉到0.8,但实际成交可能只有10手。
def weighted_imbalance(df, weights=[0.5, 0.3, 0.2]):
"""
加权订单簿不平衡度
"""
bid_depth = sum(df[f'bid_size_{i}'] * w for i, w in enumerate(weights, 1))
ask_depth = sum(df[f'ask_size_{i}'] * w for i, w in enumerate(weights, 1))
imbalance = (bid_depth - ask_depth) / (bid_depth + ask_depth + 1e-8)
return imbalance
4.3 VPIN:订单流毒性的“CT机”
VPIN(Volume-synchronized Probability of Informed Trading),翻译过来就是“成交量同步知情交易概率”。名字很唬人,但核心思想很简单:把成交量分成若干桶(bucket),每个桶里统计买卖方向的不平衡度。如果某个桶里买卖方向极度失衡,说明可能有知情交易者在动手。
计算步骤:
- 把成交量分成等量的桶(比如每个桶1000手)
- 每个桶内,用Tick数据判断每笔交易是买方主动还是卖方主动
- 统计每个桶的买卖不平衡度
- 取最近N个桶的不平衡度标准差,就是VPIN
def compute_vpin(trades, bucket_volume=1000, n_buckets=50):
"""
计算VPIN
trades: 包含 volume, side 的DataFrame(side: 1=买, -1=卖)
"""
trades['cum_vol'] = trades['volume'].cumsum()
trades['bucket'] = trades['cum_vol'] // bucket_volume
# 每个桶内计算买卖不平衡
bucket_stats = trades.groupby('bucket').agg(
buy_vol=('volume', lambda x: x[trades.loc[x.index, 'side']==1].sum()),
sell_vol=('volume', lambda x: x[trades.loc[x.index, 'side']==-1].sum())
)
bucket_stats['imbalance'] = abs(bucket_stats['buy_vol'] - bucket_stats['sell_vol']) / bucket_volume
# VPIN = 最近N个桶的imbalance均值
vpin = bucket_stats['imbalance'].rolling(n_buckets).mean().iloc[-1]
return vpin
4.4 买卖压力:微观结构的“压力测试”
买卖压力,我把它理解为“订单簿的压强”。公式是:买卖压力 = (买方主动成交量 - 卖方主动成交量) / 总成交量。但这里有个细节——要用Tick级别的逐笔数据,而不是快照数据。
为什么?因为快照数据只能看到挂单,看不到真实成交的“推力”。比如一个1000手的买单,如果它被拆成100笔10手的小单慢慢吃掉,那压力是逐渐释放的;但如果是一笔1000手的市价单直接吃掉,那就是瞬间的“高压”。
实战中我这样用:
- 计算过去1分钟内的买卖压力
- 如果压力持续为正且增大,说明买方在主动吃货,价格大概率要涨
- 如果压力突然由正转负,可能是主力在出货,要警惕
def buy_sell_pressure(trades, window='1min'):
"""
计算买卖压力
"""
trades['datetime'] = pd.to_datetime(trades['timestamp'], unit='ms')
trades.set_index('datetime', inplace=True)
# 按时间窗口聚合
resampled = trades.resample(window).agg(
buy_vol=('volume', lambda x: x[trades.loc[x.index, 'side']==1].sum()),
sell_vol=('volume', lambda x: x[trades.loc[x.index, 'side']==-1].sum())
)
resampled['pressure'] = (resampled['buy_vol'] - resampled['sell_vol']) / (resampled['buy_vol'] + resampled['sell_vol'] + 1e-8)
return resampled['pressure']
4.5 特征组合:1+1 > 2
单独用某个特征,效果有限。我习惯把斜率、不平衡度、VPIN、买卖压力组合起来,构建一个“市场状态向量”。比如:
| 状态 | 斜率 | 不平衡度 | VPIN | 买卖压力 | 策略建议 |
|---|---|---|---|---|---|
| 强势上涨 | 买方陡峭 | > 0.6 | 低 | 持续正 | 追多 |
| 虚假突破 | 买方陡峭 | > 0.6 | 高 | 突然转负 | 观望或做空 |
| 流动性枯竭 | 双侧陡峭 | 接近0 | 极高 | 剧烈波动 | 暂停交易 |
你看,同样的斜率和不平衡度,加上VPIN和买卖压力后,就能区分出“真突破”还是“假突破”。这就是微观结构特征的威力。
4.6 本章知识体系
下面这张图,是我自己梳理的本章知识结构。你可以把它当作一个“特征工程地图”,方便以后查阅。
好了,这一节的内容就到这。这些特征看起来多,但用起来其实就那几行代码。关键是理解每个特征背后的市场逻辑——为什么它有效?什么时候会失效?想通了这些,你就能在实战中灵活运用了。
公众号:蓝海数据掘金营,微信deep3321