📝

Draft article

This draft is visible to admins and superusers only. Sign in with an authorized account.

← 返回文章列表
August 23, 2026
5 分鐘閱讀

沒有買賣方標記時的交易分類:Tick、Quote、Lee-Ready、BVC

沒有買賣方標記時的交易分類:Tick、Quote、Lee-Ready、BVC
#microstructure
#trade-classification
#Lee-Ready
#order-flow
#deep-learning

先把多數微結構教科書開頭所處理的那個版本放到一邊。在本部落格實際交易的交易場所,交易方向不是需要推斷的東西——它本身就是一個欄位。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 的資料流建立帶符號訂單流,就不需要本文。

你會在四種情況下需要本文,而它們普遍到值得重視:

  1. 你只有棒資料。 歷史 OHLCV 是多數供應商販售、也是多數回測使用的資料;交易串流從未封存,或其成本高於研究價值。這就是下文的 BVC 問題。
  2. 交易場所沒有標記買賣方。 較小型的 CEX、股票 TAQ 類資料集,以及幾乎所有 2017 年以前的封存資料,都只提供價格與數量。
  3. DEX 與鏈上流。 交換事件會說明哪個代幣往哪個方向移動,但要在路由的多跳交易或聚合器中歸因攻擊方,沒有可依賴的標記。
  4. 你在重現已發表的研究。 Kyle lambda 估計、VPIN、價差分解——幾乎整個微結構文獻都建立在 Lee-Ready 分類資料上,因此要保持可比性,就必須執行它們的分類器,而不是讀取真實買賣方。

以下會介紹規則集、最重要的失效模式、批量成交量捷徑,以及測量全部方法的協議——因為加密市場提供了股票文獻從未擁有的標籤。

Tick 檢驗

Tick 檢驗事件序列

比較當前成交價格 PtP_t 與上一筆價格:

dttick={+1if Pt>Pt11if Pt<Pt1dt1tickif Pt=Pt1d_t^{\text{tick}} = \begin{cases} +1 & \text{if } P_t > P_{t-1} \\ -1 & \text{if } P_t < P_{t-1} \\ d_{t-1}^{\text{tick}} & \text{if } P_t = P_{t-1} \end{cases}

直覺是:買方發起的成交會抬高賣價並推高價格;賣方發起的成交會打到買價並壓低價格。零 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 規則

位於買賣價幾何中的成交

不比較連續成交,而是將成交價格與當時的中間價 Mt=(At+Bt)/2M_t = (A_t + B_t)/2 比較:

dtquote={+1if Pt>Mt1if Pt<Mtundefinedif Pt=Mtd_t^{\text{quote}} = \begin{cases} +1 & \text{if } P_t > M_t \\ -1 & \text{if } P_t < M_t \\ \text{undefined} & \text{if } P_t = M_t \end{cases}
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 檢驗。

dtLR={dtquoteif PtMtdttickif Pt=Mtd_t^{\text{LR}} = \begin{cases} d_t^{\text{quote}} & \text{if } P_t \neq M_t \\ d_t^{\text{tick}} & \text{if } P_t = M_t \end{cases}
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 進行拆分:

Vtbuy=VtΦ(ΔPtσΔP),Vtsell=VtVtbuyV_t^{\text{buy}} = V_t \cdot \Phi\left(\frac{\Delta P_t}{\sigma_{\Delta P}}\right), \qquad V_t^{\text{sell}} = V_t - V_t^{\text{buy}}

其中 ΔPt=PtclosePtopen\Delta P_t = P_t^{\text{close}} - P_t^{\text{open}},而 σΔP\sigma_{\Delta P} 是棒價格變化的標準差。

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 無法提供。它在棒上運作而不是逐筆記錄上,這正是重點:它是本文唯一在不存在成交級資料時仍能運作的方法。並且 σ\sigma 是會改變結果的自由參數——全樣本估計會造成前視,而短期滾動估計會讓 Φ\Phi 飽和,將每根棒推向 0/100。這個選擇值得進行敏感度掃描,而不是設定一個預設值。

原始論文報告 BVC 的追蹤結果與成交級分類足夠接近,可用於 VPIN 估計。具體準確率高度取決於棒大小、工具與 σ\sigma 估計器,而二手資料引用的數字很少同時說明這三項。不要把股票研究中的一個數字直接帶過來——請測量。

分類專用的唯一特徵

突出的微結構訊號

如果你要訓練模型而不是執行規則,大部分特徵集已經發布:價差、深度不平衡、成交流不平衡,以及滾動實現波動率,都在使用機器學習建模價差中列出並實作;DeepLOB則提供多層訂單簿特徵。使用它們。

這個問題專有、且兩者都沒有出現的特徵,是成交在價差內的位置:

relative_pricet=PtBtAtBt\text{relative\_price}_t = \frac{P_t - B_t}{A_t - B_t}

成交位於買價時映射為 0,位於賣價時為 1,位於中間價時為 0.5。這是 Quote 規則的連續推廣:不在中間價設門檻,而是把距離交給模型,讓模型學到 0.95 的成交幾乎確定是買方,而 0.55 的成交幾乎沒有資訊。落在 [0,1][0, 1] 之外的值,就是上一節所述的過時報價診斷,值得記錄而非截斷。

若要在這些特徵上建立提升式基準,管線就是使用機器學習建模價差中已發布的版本,只將目標換成二元分類——尤其包括其中清洗與禁運的 walk-forward CV。不要使用普通的 TimeSeriesSplit 評估微結構分類器;該文與walk-forward 最佳化都解釋了為何這會產生過度樂觀的數字。若是對訂單簿快照使用序列模型,DeepLOB 是參考架構,其中的訓練注意事項保持不變。

正確測量方法:加密市場提供標籤

帶標籤的加密市場資料流

股票交易分類文獻中的每個準確率,都是相對於真實值的 代理 估計——TORQ、稽核軌跡子樣本、單一交易場所的內部紀錄——因為真正的攻擊方從未公開。加密市場公開了它。Binance aggTradesisBuyerMaker 就是每筆成交的標籤,且完全免費。因此任何人都能進行這個實驗,而我還沒有找到針對加密市場發表的結果。協議如下:

資料。 一個月的 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 的 σ\sigma 是估計值,因此其誤差按構造就取決於狀態)。

方法 總體 中間價 緊價差 寬價差 大額成交
Tick 檢驗
Quote 規則
Lee-Ready(延遲 0)
Lee-Ready(延遲 5 秒)
BVC(每棒比例) 不適用 不適用

準確率不是目標

分類邊界與執行影響

借用 DeepLOB 的論述紀律:分類準確率不是結果,而是結果的輸入。沒有人交易一筆成交的符號——交易的是從完整符號串流計算出的某些東西,而錯誤不會中性地傳播。一個在 10% 成交上隨機錯誤的分類器,與另一個系統性錯誤地把 10% 的大型、推動價格成交標錯的分類器,在準確率相同時會產生非常不同的帶符號訂單流。Tick 檢驗的前向延續失效與平坦價格特別相關——而這正是帶符號流應該具有資訊量的狀態。因此,上述拆解而非標題數字,才是真正的輸出。

有了真實標籤,下游問題就能直接測量:從真實符號估計你關心的量,再從每個分類器的符號重新估計,報告差距。

  • 價格影響。 在固定窗口上以帶符號淨吃單流回歸中間價變化——估計器已在滑價成本模型中指定,而其在公開 Binance 資料上的可運作實作位於Almgren-Chriss 最佳執行。不要在這裡重新定義;執行兩次並比較擬合係數。
  • 以訂單流不平衡作為預測器。 比較真實符號與分類符號下 OFI 和未來報酬的相關性。該相關性的退化 就是 分類錯誤的成本,並以唯一重要的單位計價。
  • 毒性指標。 VPIN 與成交流不平衡特徵都是分類成交量的使用者;兩者都在使用機器學習建模價差中介紹。它們對分類錯誤的敏感性,可以用相同方式測量。

這對你意味著什麼

已解決的訂單流地圖

如果你的交易場所提供攻擊方標記,就使用它。大多數時候,加密市場都是如此。

如果沒有,使用同時報價下的 Lee-Ready 作為預設是正確的——不是因為它準確,而是因為它是標準,與三十年已發表估計保持可比性,比你無法驗證的幾個準確率點更有價值。五秒規則是 1980 年代行情延遲的化石;在現代資料上重現它,會注入它原本要移除的過時性。如果你只有棒資料,BVC 是唯一完全可行的選項,而且它回答的是另一個問題:成交量拆分,而不是成交符號。

無論如何,都應該用加密市場免費提供的標籤進行測量,按價差狀態與成交量拆解結果,並報告錯誤在下游造成的成本,而不是報告猜對的百分比。

免責宣告:本文提供的資訊僅用於教育和參考目的,不構成財務、投資或交易建議。加密貨幣交易涉及重大損失風險。

Authors

Eugen Soloviov
Eugen Soloviov

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.

Newsletter

緊跟市場步伐

訂閱我們的時事通訊,獲取獨家 AI 交易見解、市場分析和平台更新。

我們尊重您的隱私。您可以隨時退訂。