4、信号计算引擎:编写Python类实时计算买卖压力差、累积失衡度、加权失衡比

好,咱们进入正题。

前面几章我们聊了订单簿的基础结构,也讲了怎么抓取和清洗数据。但说实话,那些都只是准备工作。真正让策略跑起来、让信号变成交易决策的,是这一章要讲的——信号计算引擎

我个人习惯把信号计算引擎比作「订单簿的翻译官」。原始数据是密密麻麻的挂单,普通人看着眼晕。但引擎能把这些数字翻译成三个核心指标:买卖压力差累积失衡度加权失衡比。这三个指标,基本能覆盖90%的订单簿失衡场景。

核心观点:订单簿失衡的本质,是买卖双方在某一价位区间内的「兵力对比」出现了倾斜。我们的任务,就是用数学语言把这个倾斜程度量化出来。

4.1 三个指标,各司其职

先别急着写代码。咱们得先搞清楚,这三个指标到底在算什么。

指标名称 英文缩写 核心含义 典型用法
买卖压力差 PSD (Pressure Spread Difference) 当前档位买盘总量 vs 卖盘总量的差值 判断当前瞬间的多空力量对比
累积失衡度 CID (Cumulative Imbalance Degree) 从第1档到第N档的累积买卖量差值 判断深度方向上的持续失衡
加权失衡比 WIR (Weighted Imbalance Ratio) 按距离最优价的距离加权后的失衡比例 消除「挂单陷阱」,更真实反映意图

嗯,这里要注意。很多新手只盯着买卖压力差看,觉得正数就是买盘强,负数就是卖盘强。其实不然。我在项目中遇到过好几次,买卖压力差明明是正的,价格却往下砸。为什么?因为有人在第5档、第6档偷偷挂了大单,压力差没算进去。这就是为什么我们需要累积失衡度和加权失衡比。

4.2 代码实现:OrderBookImbalance 类

好,直接上代码。我习惯把信号计算引擎封装成一个类,这样在回测和实盘里都能复用。

import numpy as np
from typing import List, Tuple, Optional

class OrderBookImbalance:
    """
    订单簿失衡信号计算引擎
    支持实时计算买卖压力差、累积失衡度、加权失衡比
    """
    
    def __init__(self, depth: int = 10):
        """
        :param depth: 计算深度,默认取前10档
        """
        self.depth = depth
        self._last_psd = 0.0
        self._last_cid = 0.0
        self._last_wir = 0.0
        
    def compute_psd(self, bids: List[Tuple[float, float]], 
                    asks: List[Tuple[float, float]]) -> float:
        """
        计算买卖压力差 (Pressure Spread Difference)
        
        :param bids: 买盘数据,格式 [(price, volume), ...]
        :param asks: 卖盘数据,格式 [(price, volume), ...]
        :return: 压力差值,正数表示买盘强,负数表示卖盘强
        """
        # 取前depth档
        bids_slice = bids[:self.depth]
        asks_slice = asks[:self.depth]
        
        # 计算总买量和总卖量
        total_bid_vol = sum(v for _, v in bids_slice)
        total_ask_vol = sum(v for _, v in asks_slice)
        
        # 压力差 = 买盘总量 - 卖盘总量
        psd = total_bid_vol - total_ask_vol
        
        self._last_psd = psd
        return psd
    
    def compute_cid(self, bids: List[Tuple[float, float]], 
                    asks: List[Tuple[float, float]], 
                    levels: int = 5) -> float:
        """
        计算累积失衡度 (Cumulative Imbalance Degree)
        
        :param bids: 买盘数据
        :param asks: 卖盘数据
        :param levels: 累积计算的档位数
        :return: 累积失衡度
        """
        # 取前levels档
        bids_slice = bids[:levels]
        asks_slice = asks[:levels]
        
        # 逐档累积差值
        cumulative_diff = 0.0
        for i in range(min(len(bids_slice), len(asks_slice))):
            bid_vol = bids_slice[i][1]
            ask_vol = asks_slice[i][1]
            cumulative_diff += (bid_vol - ask_vol)
        
        self._last_cid = cumulative_diff
        return cumulative_diff
    
    def compute_wir(self, bids: List[Tuple[float, float]], 
                    asks: List[Tuple[float, float]], 
                    decay_factor: float = 0.9) -> float:
        """
        计算加权失衡比 (Weighted Imbalance Ratio)
        
        使用指数衰减权重,越靠近最优价的档位权重越大
        
        :param bids: 买盘数据
        :param asks: 卖盘数据
        :param decay_factor: 衰减因子,默认0.9
        :return: 加权失衡比,范围[-1, 1]
        """
        # 计算加权买量
        weighted_bid = 0.0
        for i, (_, vol) in enumerate(bids[:self.depth]):
            weight = decay_factor ** i
            weighted_bid += vol * weight
        
        # 计算加权卖量
        weighted_ask = 0.0
        for i, (_, vol) in enumerate(asks[:self.depth]):
            weight = decay_factor ** i
            weighted_ask += vol * weight
        
        # 防止除零
        total = weighted_bid + weighted_ask
        if total == 0:
            return 0.0
        
        # 加权失衡比 = (加权买量 - 加权卖量) / (加权买量 + 加权卖量)
        wir = (weighted_bid - weighted_ask) / total
        
        self._last_wir = wir
        return wir
    
    def compute_all(self, bids: List[Tuple[float, float]], 
                    asks: List[Tuple[float, float]]) -> dict:
        """
        一次性计算所有指标
        """
        return {
            'psd': self.compute_psd(bids, asks),
            'cid': self.compute_cid(bids, asks),
            'wir': self.compute_wir(bids, asks)
        }

个人经验:decay_factor 这个参数我踩过坑。一开始我设成0.8,结果信号太敏感,频繁假突破。后来改成0.95,又太迟钝,错过了几波行情。最终在BTC永续合约上调试下来,0.9是个不错的平衡点。你根据自己的品种微调就行。

4.3 三个指标的计算逻辑详解

代码贴完了,咱们拆开揉碎了讲一讲。

4.3.1 买卖压力差(PSD)

这个最简单。就是把买一到买十的总量加起来,减去卖一到卖十的总量。说白了就是「当前所有明牌的多头兵力减去空头兵力」。正值说明买盘人多,负值说明卖盘人多。

但我要提醒你——这个指标有个致命缺陷。它把所有档位的挂单量一视同仁。你想想看,第1档挂1个BTC和第10档挂1个BTC,对价格的冲击能一样吗?显然不一样。所以光看PSD是不够的。

4.3.2 累积失衡度(CID)

CID解决了「只看表面」的问题。它从第1档开始,逐档累加买卖差值。比如你算前5档的CID,就是第1档差值 + 第2档差值 + ... + 第5档差值。

这样做的好处是,你能看到失衡是在浅层还是深层。我曾经在ETH的订单簿上发现,PSD是正的(买盘总量大),但CID从第3档开始就变成负的了。这说明什么?说明有人在浅层挂小单诱多,深层偷偷挂大单准备砸盘。这种信号,光看PSD根本发现不了。

4.3.3 加权失衡比(WIR)

WIR是我个人最看重的指标。它引入了权重概念——离最优价越近的档位,权重越大。用指数衰减的方式,让第1档的权重是第2档的0.9倍,第2档是第3档的0.9倍,以此类推。

为什么这么做?因为离最优价越近的挂单,越能反映交易者的真实意图。有人愿意在买一价挂单,说明他真的想在这个价位买入。而在买十价挂单的人,可能只是挂个钓鱼单,根本不指望成交。

避坑指南:我曾经在回测中发现,WIR在盘口价差特别大的时候会失真。比如某个流动性差的币种,买一和卖一之间差了0.5%,这时候第1档的权重过大,导致WIR剧烈波动。我的解决办法是:先判断价差是否超过阈值(比如0.1%),如果超过,就改用等权重的CID。

4.4 信号计算引擎的完整流程图

下面这张图,是我做这个引擎时的设计思路。你看一眼就能明白整个计算流程。

订单簿失衡信号计算引擎流程图 原始订单簿数据 数据清洗 & 深度截取 PSD计算 买总量 - 卖总量 CID计算 逐档累积差值 WIR计算 指数加权失衡比 输出:PSD / CID / WIR 用于策略信号生成

4.5 实战中的使用技巧

代码和流程图都有了,最后聊几个实战技巧。

  • 多时间框架验证:别只看当前tick的失衡值。我习惯把过去10个tick的PSD、CID、WIR存下来,算一个滚动均值。如果当前值偏离均值超过2个标准差,才认为是有效信号。
  • 结合成交量验证:失衡信号出来之后,如果成交量没有同步放大,很可能是假信号。我在项目中遇到过好几次,失衡值很大但成交量萎缩,结果价格根本没动。
  • 参数自适应:不同币种、不同行情阶段,depth和decay_factor应该不一样。波动大的时候depth设小一点(比如5档),波动小的时候设大一点(比如20档)。

一句话总结:PSD看表面,CID看深度,WIR看真实意图。三个指标配合使用,才能把订单簿的「潜台词」读出来。


专注资料整理