沒有買賣方標記時的交易分類:Tick、Quote、Lee-Ready、BVC
先把多數微結構教科書開頭所處理的那個版本放到一邊。在本部落格實際交易的交易場所,交易方向不是需要推斷的東西——它本身就是一個欄位。Binance 的 aggTrades 提供 isBuyerMaker;Bybit 的成交串流提供 side。棒構建文章 直接從該標記取得攻擊方,不會執行分類器:
def on_trade(self, timestamp: int, price: float, qty: float, is_buyer_maker: bool):
side = -1 if is_buyer_maker else 1
這是免費且具有奈秒解析度的真實標籤。如果你正從現代加密 CEX 的資料流建立帶符號訂單流,就不需要本文。
你會在四種情況下需要本文,而它們普遍到值得重視:
- 你只有棒資料。 歷史 OHLCV 是多數供應商販售、也是多數回測使用的資料;交易串流從未封存,或其成本高於研究價值。這就是下文的 BVC 問題。
- 交易場所沒有標記買賣方。 較小型的 CEX、股票 TAQ 類資料集,以及幾乎所有 2017 年以前的封存資料,都只提供價格與數量。
- DEX 與鏈上流。 交換事件會說明哪個代幣往哪個方向移動,但要在路由的多跳交易或聚合器中歸因攻擊方,沒有可依賴的標記。
- 你在重現已發表的研究。 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
具備相同前向延續語義的生產實作,已經以 _tick_sign() 形式存在於超越時間棒的 tick 與成交量不平衡棒生成器中——請重用它,不要維護第二份副本。
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 不只是提出混合方法;他們還主張將每筆成交與 五秒前有效的報價 比較。這個數字不是關於市場的建模選擇,而是對報告延遲人為因素的修正:在 1980 年代 NYSE 的綜合行情中,成交與報價透過不同路徑傳播、延遲也不同,報價往往在所屬成交之前抵達。因此,將成交與同時列印的報價比較,實際上是與已反映該成交的報價比較——恰好對推動市場的成交反轉了符號。
這對任何把方法移植到加密市場的人帶來兩點後果。
五秒規則已經過時,套用它反而有害。 它修正的是如今已不存在的傳播延遲。股票市場在 1990 年代中期前後就縮小了差距;加密交易場所的成交與訂單簿增量串流共享序列空間,並由同一撮合引擎以微秒精度加時間戳。BTCUSDT 的五秒不是同步容忍度,而是數百次訂單簿更新。讓報價延遲五秒不會修正過時報價,只會製造過時報價。除非你已在自己的擷取資料中測量出真實偏移,否則使用 quote_lag=0。
但底層問題以新形式存續。 現在報價過時是你的資料管線造成的,不是交易所的錯。從 WebSocket 增量串流重建訂單簿,按掛鐘時間連接成交,你就引入了延遲;其方向與大小取決於緩衝、快照重新同步策略與時鐘。在信任任何讀取訂單簿的分類器前,先測量它:重播一段窗口,把成交連接到訂單簿狀態,並檢查有多少成交落在連接後的 [bid, ask] 之外。若一筆成交高於連接後的賣價,就證明與它匹配的訂單簿已過時。
修正方法不同於 Lee 與 Ready,但失效模式相同。
批量成交量分類

Easley、Lopez de Prado 與 O'Hara(2012)攻擊的是另一個問題:如果你從未看見個別成交怎麼辦?BVC 在棒內分類聚合成交量,以標準化價格變化的標準常態 CDF 進行拆分:
其中 ,而 是棒價格變化的標準差。
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 是不同的工具,而不是較差的工具,三項特性可以說明原因。它不分類單筆成交——它產生的是一個 比例,因此收盤不變的棒會得到 50/50 拆分,而不是對其中每筆成交擲一次硬幣;如果你的使用者需要每筆成交的符號,BVC 無法提供。它在棒上運作而不是逐筆記錄上,這正是重點:它是本文唯一在不存在成交級資料時仍能運作的方法。並且 是會改變結果的自由參數——全樣本估計會造成前視,而短期滾動估計會讓 飽和,將每根棒推向 0/100。這個選擇值得進行敏感度掃描,而不是設定一個預設值。
原始論文報告 BVC 的追蹤結果與成交級分類足夠接近,可用於 VPIN 估計。具體準確率高度取決於棒大小、工具與 估計器,而二手資料引用的數字很少同時說明這三項。不要把股票研究中的一個數字直接帶過來——請測量。
分類專用的唯一特徵

如果你要訓練模型而不是執行規則,大部分特徵集已經發布:價差、深度不平衡、成交流不平衡,以及滾動實現波動率,都在使用機器學習建模價差中列出並實作;DeepLOB則提供多層訂單簿特徵。使用它們。
這個問題專有、且兩者都沒有出現的特徵,是成交在價差內的位置:
成交位於買價時映射為 0,位於賣價時為 1,位於中間價時為 0.5。這是 Quote 規則的連續推廣:不在中間價設門檻,而是把距離交給模型,讓模型學到 0.95 的成交幾乎確定是買方,而 0.55 的成交幾乎沒有資訊。落在 之外的值,就是上一節所述的過時報價診斷,值得記錄而非截斷。
若要在這些特徵上建立提升式基準,管線就是使用機器學習建模價差中已發布的版本,只將目標換成二元分類——尤其包括其中清洗與禁運的 walk-forward CV。不要使用普通的 TimeSeriesSplit 評估微結構分類器;該文與walk-forward 最佳化都解釋了為何這會產生過度樂觀的數字。若是對訂單簿快照使用序列模型,DeepLOB 是參考架構,其中的訓練注意事項保持不變。
正確測量方法:加密市場提供標籤

股票交易分類文獻中的每個準確率,都是相對於真實值的 代理 估計——TORQ、稽核軌跡子樣本、單一交易場所的內部紀錄——因為真正的攻擊方從未公開。加密市場公開了它。Binance aggTrades 的 isBuyerMaker 就是每筆成交的標籤,且完全免費。因此任何人都能進行這個實驗,而我還沒有找到針對加密市場發表的結果。協議如下:
資料。 一個月的 BTCUSDT aggTrades 加上訂單簿 ticker 串流,進行連接,使每筆成交都帶有它之前一刻有效的買價與賣價。在流動性薄的山寨幣上重做,因為整個問題就在於價差變寬時會發生什麼。
標籤。 direction = -1 if is_buyer_maker else +1。接著 丟棄標記,只用價格與報價資料執行各種方法。
方法。 Tick 檢驗、Quote 規則、quote_lag=0 的 Lee-Ready、歷史五秒延遲的 Lee-Ready(量化過時規則造成的損害),以及在多種棒大小下的 BVC,並與真實的每棒買方成交量比例比較。
拆解。 總體準確率是最不有趣的數字。按價差狀態拆解(單 tick 對寬價差——價差被鎖定時 Tick 檢驗的前向延續應該崩潰)、按成交是否列印在中間價拆解(隔離 Quote 規則棄權、而 Lee-Ready 備援完成全部工作的情況)、按成交量拆解(aggTrades 條目是掃單;大型成交會推動影響,方向標錯時成本最高),以及按波動率狀態拆解(BVC 的 是估計值,因此其誤差按構造就取決於狀態)。
| 方法 | 總體 | 中間價 | 緊價差 | 寬價差 | 大額成交 |
|---|---|---|---|---|---|
| Tick 檢驗 | — | — | — | — | — |
| Quote 規則 | — | — | — | — | — |
| Lee-Ready(延遲 0) | — | — | — | — | — |
| Lee-Ready(延遲 5 秒) | — | — | — | — | — |
| BVC(每棒比例) | — | 不適用 | — | — | 不適用 |
準確率不是目標

借用 DeepLOB 的論述紀律:分類準確率不是結果,而是結果的輸入。沒有人交易一筆成交的符號——交易的是從完整符號串流計算出的某些東西,而錯誤不會中性地傳播。一個在 10% 成交上隨機錯誤的分類器,與另一個系統性錯誤地把 10% 的大型、推動價格成交標錯的分類器,在準確率相同時會產生非常不同的帶符號訂單流。Tick 檢驗的前向延續失效與平坦價格特別相關——而這正是帶符號流應該具有資訊量的狀態。因此,上述拆解而非標題數字,才是真正的輸出。
有了真實標籤,下游問題就能直接測量:從真實符號估計你關心的量,再從每個分類器的符號重新估計,報告差距。
- 價格影響。 在固定窗口上以帶符號淨吃單流回歸中間價變化——估計器已在滑價成本模型中指定,而其在公開 Binance 資料上的可運作實作位於Almgren-Chriss 最佳執行。不要在這裡重新定義;執行兩次並比較擬合係數。
- 以訂單流不平衡作為預測器。 比較真實符號與分類符號下 OFI 和未來報酬的相關性。該相關性的退化 就是 分類錯誤的成本,並以唯一重要的單位計價。
- 毒性指標。 VPIN 與成交流不平衡特徵都是分類成交量的使用者;兩者都在使用機器學習建模價差中介紹。它們對分類錯誤的敏感性,可以用相同方式測量。
這對你意味著什麼

如果你的交易場所提供攻擊方標記,就使用它。大多數時候,加密市場都是如此。
如果沒有,使用同時報價下的 Lee-Ready 作為預設是正確的——不是因為它準確,而是因為它是標準,與三十年已發表估計保持可比性,比你無法驗證的幾個準確率點更有價值。五秒規則是 1980 年代行情延遲的化石;在現代資料上重現它,會注入它原本要移除的過時性。如果你只有棒資料,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.