Trade Classification When You Have No Side Flag: Tick, Quote, Lee-Ready, BVC
先排除大多数微观结构教材开篇讨论的那种情况。在本博客实际交易的交易场所,交易方向不是需要推断的东西,而是一个字段。Binance aggTrades 提供 isBuyerMaker,Bybit 的交易流提供 side。Bar 构造文章直接从该标记取得主动方一侧,从不运行分类器:
def on_trade(self, timestamp: int, price: float, qty: float, is_buyer_maker: bool):
side = -1 if is_buyer_maker else 1
这是免费的真值,而且精度达到纳秒级。如果你要从现代加密货币 CEX 流构造带符号的订单流,就不需要本文。
以下四种情况需要本文,而且它们常见到足以值得关注:
- 你只有 bar。 历史 OHLCV 是大多数供应商出售、也是大多数回测运行的数据;交易流从未存档,或者存储成本超过了研究价值。这就是下文的 BVC 问题。
- 交易场所没有方向标记。 较小的 CEX、股票 TAQ 风格数据集,以及几乎所有 2017 年以前的存档,都只提供价格和规模。
- DEX 与链上流。 swap 事件说明哪个代币向哪个方向移动,但在路由的多跳交易或聚合器中归因主动方,没有可回退的标记。
- 你在复现已发表的研究。 Kyle lambda 估计、VPIN、价差分解——几乎整个微观结构文献都建立在 Lee-Ready 分类数据上,因此要实现可比性,就必须运行它的分类器,而不是读取真实方向。
下文依次介绍规则集、最重要的失效模式、批量成交量捷径,以及测量全部方法的协议——因为加密货币把股票文献从未拥有的标签直接交给了你。
Tick 检验

将当前成交价 与上一笔比较:
直觉是:买方主动的交易抬高卖价并推动价格上升;卖方主动的交易打到买价并推动价格下降。零 tick 会沿用前一个符号,这正是大多数误差的来源——在价差为一个 tick 的流动性工具上,大多数连续成交的打印价格都相同。
sign = np.sign(np.diff(prices, prepend=prices[0]))
sign[sign == 0] = np.nan
directions = pd.Series(sign).ffill().fillna(1).astype(int).values
生产实现中已经有相同的向前携带语义,位于超越时间 bar的 tick 和成交量不平衡 bar 生成器内部 _tick_sign()。应复用它,而不是维护第二份代码。
Quote 规则

不比较连续成交,而是将成交价与当时的中间价 比较:
def quote_rule(trade_prices: np.ndarray,
bid_prices: np.ndarray,
ask_prices: np.ndarray) -> np.ndarray:
"""Classify trades using the quote rule.
Returns +1 (buy), -1 (sell), or 0 (unclassified) for midpoint trades.
"""
midpoints = (bid_prices + ask_prices) / 2.0
directions = np.zeros(len(trade_prices), dtype=int)
directions[trade_prices > midpoints] = 1
directions[trade_prices < midpoints] = -1
return directions
Quote 规则使用的信息严格多于 tick 检验,但在另一处失效:它对恰好打印在中间价的交易无能为力。在窄价差且受 tick 约束的工具上,这并非边缘情况,而是交易带中很大的一部分。该规则需要回退方案,这正是 Lee-Ready 的作用。
Lee-Ready:混合方法与过时的报价假设

Lee 和 Ready(1991)将两者结合:应用 Quote 规则,对位于中间价的交易回退到 Tick 检验。
def lee_ready(trade_prices: np.ndarray,
bid_prices: np.ndarray,
ask_prices: np.ndarray,
quote_lag: int = 0) -> np.ndarray:
"""Lee-Ready classification.
quote_lag : number of quote observations to lag (0 = contemporaneous).
Set > 0 only if your quote feed is known to trail the trade
feed; see the discussion of the 5-second rule below.
Depends on tick_test() with carry-forward semantics — the `_tick_sign()`
implementation from /blog/beyond-time-bars-candle-construction.
"""
if quote_lag > 0:
bid = np.concatenate([bid_prices[:quote_lag], bid_prices[:-quote_lag]])
ask = np.concatenate([ask_prices[:quote_lag], ask_prices[:-quote_lag]])
else:
bid, ask = bid_prices, ask_prices
directions = quote_rule(trade_prices, bid, ask)
unclassified = directions == 0
if unclassified.any():
directions[unclassified] = tick_test(trade_prices)[unclassified]
return directions
论文更有趣的一半,通常在复现时会失去其论证。Lee 和 Ready 不只是提出了混合方法,还提出将每笔交易与五秒前有效的报价比较。这个数字不是关于市场的建模选择,而是对报告伪影的修正:在 20 世纪 80 年代的 NYSE 综合行情中,交易和报价通过延迟不同的路径传播,报价往往先于其所属交易到达。因此,将交易与同时打印的报价比较,实际上是与已经反映该交易的报价比较——会恰好反转推动市场的那些交易的符号。
对于把它移植到加密货币的任何人,有两点结论。
五秒规则已经过时,应用它反而有害。 它修正的是已经不存在的传播延迟。股票市场在 20 世纪 90 年代中期已经缩小了这个差距;加密货币交易场所的交易流和订单簿增量流共享序列空间,并由同一个撮合引擎以微秒精度打时间戳。对 BTCUSDT 而言,五秒不是同步容差,而是数百次订单簿更新。将报价滞后五秒不能修复过时报价,只会制造过时报价。除非你已经在自己的采集数据中测得真实偏移,否则应使用 quote_lag=0。
但底层问题以新的形式继续存在。 报价过时现在是你的数据管道造成的,而不是交易所造成的。从 WebSocket 增量流重建订单簿,按墙上时钟时间连接交易,你就引入了一个符号和大小取决于缓冲、快照重同步策略及时钟的延迟。在信任任何读取订单簿的分类器之前要测量它:回放一个窗口,将交易连接到订单簿状态,并检查有多少交易落在连接后的 [bid, ask] 之外。高于连接后卖价的交易,证明你匹配到的是一个过时的订单簿。
修复方式不同于 Lee 和 Ready 的方式,但失效模式相同。
批量成交量分类

Easley、Lopez de Prado 和 O'Hara(2012)处理的是另一个问题:如果你从未看到单笔交易怎么办?BVC 在一个 bar 内对汇总成交量进行分类,使用标准化价格变化的标准正态 CDF 进行切分:
其中 , 是 bar 价格变化的标准差。
from scipy.stats import norm
def bulk_volume_classification(open_prices: np.ndarray,
close_prices: np.ndarray,
volumes: np.ndarray,
sigma: float | None = None) -> tuple:
"""Split bar volume into buy/sell using the BVC rule.
Returns (buy_volume, sell_volume) as continuous, not integer, splits.
"""
delta_p = close_prices - open_prices
if sigma is None:
sigma = max(float(np.std(delta_p)), 1e-8)
buy_fraction = norm.cdf(delta_p / sigma)
return volumes * buy_fraction, volumes * (1.0 - buy_fraction)
BVC 是不同的工具,不是更差的工具,三个特性可以说明原因。它不分类交易,而是产生一个比例,因此收盘不变的 bar 会得到 50/50 的切分,而不是对其中每笔交易抛硬币;如果下游使用者需要逐笔符号,BVC 无法提供。它运行在 bar 而非交易带上,这正是其意义所在:当不存在交易级数据时,它是本文唯一仍然有效的方法。而 是会改变结果的自由参数——全样本估计会产生前视,而短滚动估计会令 饱和,把每个 bar 推向 0/100。这种选择值得进行敏感性扫描,而不是设置一个默认值。
原论文报告 BVC 对交易级分类的跟踪足够接近,可以用于 VPIN 估计。具体准确率严重取决于 bar 大小、工具和 估计器,二手资料引用的数字很少同时说明这三者。不要把股票研究中的数字直接带过来——请测量。
唯一与分类相关的特征

如果训练模型而不是运行规则来完成这件事,大部分特征集已经发布:价差、深度不平衡、交易流不平衡和滚动实现波动率已在用机器学习进行价差建模中列出并实现, DeepLOB 中还有多层订单簿特征。请使用它们。
这个问题特有、且两篇文章都没有出现的特征,是交易在价差内部的位置:
位于买价的交易映射为 0,卖价为 1,中间价为 0.5。这是 Quote 规则的连续推广:不在中间价处设置阈值,而是把距离交给模型,让它学到 0.95 的交易几乎确定是买入,而 0.55 的交易几乎没有信息。 之外的值是上一节所说的过时报价诊断,值得记录而不是截断。
基于这些特征构造 boosted 基线时,使用用机器学习进行价差建模中已经发布的管道,只需将目标换成二分类——尤其要保留其中的净化和封锁式滚动前向交叉验证。不要用普通的 TimeSeriesSplit 评估微观结构分类器;该文章和滚动前向优化都解释了为什么所得数字会过于乐观。对于订单簿快照上的序列模型,DeepLOB是参考架构,其中的训练注意事项保持不变。
正确测量:加密货币提供了标签

股票交易分类文献中的每个准确率数字,都是相对于真值的代理估计的——TORQ、审计轨迹子样本、单一交易场所的内部记录——因为真实主动方从未被公开。加密货币公开了它。Binance aggTrades 中的 isBuyerMaker 就是每笔交易的标签,而且免费。因此任何人都可以进行这项实验,而我没有找到针对加密货币发布的结果。协议如下:
数据。 一个月的 BTCUSDT aggTrades 加订单簿 ticker 流,连接时让每笔交易携带其发生前一刻有效的买价和卖价。在一个流动性较薄的 alt 上重复,因为问题的核心就是价差变宽时会发生什么。
标签。 direction = -1 if is_buyer_maker else +1。然后丢弃这个标记,只用价格和报价数据运行每种方法。
方法。 Tick 检验、Quote 规则、quote_lag=0 的 Lee-Ready、历史五秒滞后的 Lee-Ready(量化过时规则造成的损害),以及多个 bar 大小下的 BVC,并与真实的逐 bar 买入成交量比例比较。
分解。 总体准确率是最不重要的数字。按价差状态分解(一个 tick 对宽价差——价差被钉在一个 tick 时,Tick 检验的向前携带应当崩溃),按交易是否打印在中间价分解(隔离 Quote 规则弃权的地方,以及 Lee-Ready 回退承担全部工作的地方),按交易规模分解(aggTrades 条目是扫单;大交易推动影响,被错误赋予符号时成本最高),按波动状态分解(BVC 的 是估计出来的,因此其误差按构造就依赖于状态)。
| 方法 | 总体 | 中间价 | 窄价差 | 宽价差 | 大额交易 |
|---|---|---|---|---|---|
| Tick 检验 | — | — | — | — | — |
| Quote 规则 | — | — | — | — | — |
| Lee-Ready(滞后 0) | — | — | — | — | — |
| Lee-Ready(5 秒滞后) | — | — | — | — | — |
| BVC(逐 bar 比例) | — | 不适用 | — | — | 不适用 |
准确率不是目标

借鉴 DeepLOB的框架纪律:分类准确率不是结果,而是结果的输入。没有人交易单笔交易的符号——他们交易的是由完整符号流计算出的东西,错误不会中性地传播。一个在 10% 交易上随机出错的分类器,与一个专门在占 10% 且规模大、会推动价格的交易上出错的分类器,会在准确率完全相同的情况下产生非常不同的带符号订单流。Tick 检验的向前携带失效与价格持平明确相关,而这正是带符号流应该最有信息量的状态。因此,上面的分解而不是标题数字,才是真正的输出。
有了真值标签,下游问题就可以直接测量:用真实符号估计你关心的量,再用每个分类器的符号重新估计,并报告差距。
- 价格影响。 在固定窗口上用带符号的净主动成交流回归中间价变化——估计器已在滑点成本模型中规定,公共 Binance 数据上的实现见Almgren-Chriss 最优执行。不要在这里重新定义;运行两次并比较拟合系数。
- 订单流不平衡作为预测变量。 计算真实符号与分类符号下 OFI 和未来收益的相关性。该相关性的退化就是分类错误的成本,并以唯一重要的单位表示。
- 毒性指标。 VPIN 和交易流不平衡特征都使用分类成交量;两者均在用机器学习进行价差建模中介绍。它们对分类错误的敏感性也可以用相同方式测量。
你最终得到什么

如果交易场所提供主动方标记,就使用它。大多数时候,加密货币都是如此。
如果没有,使用同时报价的 Lee-Ready 是正确的默认方案——不是因为它准确,而是因为它是标准;与三十年已发表估计保持可比性,比无法验证的几个准确率百分点更有价值。五秒规则是 20 世纪 80 年代行情延迟的化石,在现代数据上复现它,会注入它原本要消除的过时性。如果你只有 bar,BVC 是唯一完全可用的方案,而且它回答的是不同问题:成交量切分,而非交易符号。
无论哪种情况,都应使用加密货币免费提供的标签进行测量,按价差状态和交易规模分解结果,并报告错误在下游造成的成本,而不是报告猜对了多少百分比。
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.