📝

Draft article

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

← Retour aux articles
August 23, 2026
5 min de lecture

Trade Classification When You Have No Side Flag: Tick, Quote, Lee-Ready, BVC

Trade Classification When You Have No Side Flag: Tick, Quote, Lee-Ready, BVC
#microstructure
#trade-classification
#Lee-Ready
#order-flow
#deep-learning

Commençons par écarter la version de ce problème par laquelle s'ouvrent la plupart des manuels de microstructure. Sur les plateformes que ce blog utilise réellement, le sens de la transaction ne se déduit pas — c'est un champ. Les aggTrades de Binance fournissent isBuyerMaker ; le flux de transactions de Bybit fournit side. L'article sur la construction des barres prend directement le côté agressif dans ce champ et n'exécute jamais de classifieur :

def on_trade(self, timestamp: int, price: float, qty: float, is_buyer_maker: bool):
    side = -1 if is_buyer_maker else 1

C'est la vérité terrain, gratuitement, à la résolution de la nanoseconde. Si vous construisez un flux d'ordres signé à partir du flux d'un CEX crypto moderne, vous n'avez pas besoin de cet article.

Vous en avez besoin dans quatre situations, suffisamment courantes pour compter :

  1. Vous n'avez que des barres. L'OHLCV historique est ce que vendent la plupart des fournisseurs et sur quoi tournent la plupart des backtests ; le flux de transactions n'a jamais été archivé ou coûte plus cher que ce que vaut la recherche. C'est le problème BVC ci-dessous.
  2. La plateforme n'indique pas le côté. Les petits CEX, les jeux de données de type TAQ sur les actions et presque toutes les archives antérieures à 2017 ne fournissent que le prix et la taille.
  3. Flux DEX et on-chain. Un événement de swap indique quel token a bougé dans quel sens, mais l'attribution de l'agression dans une transaction routée à plusieurs hops ou via un agrégateur ne dispose d'aucun indicateur de secours.
  4. Vous reproduisez une recherche publiée. Estimations du lambda de Kyle, VPIN, décompositions du spread — presque toute la littérature de microstructure repose sur des données classifiées par Lee-Ready ; la comparabilité impose donc d'exécuter leur classifieur plutôt que de lire le véritable côté.

La suite présente l'ensemble des règles, le mode d'échec le plus important, le raccourci du volume agrégé et un protocole pour tout mesurer — car la crypto fournit les étiquettes dont la littérature actions n'a jamais disposé.

Le tick test

Séquence d'événements du tick test

Comparez le prix de la transaction courante PtP_t au précédent :

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}

L'intuition est la suivante : les transactions initiées par l'acheteur lèvent l'ask et poussent le prix vers le haut ; celles initiées par le vendeur frappent le bid et le poussent vers le bas. Les ticks nuls propagent le signe précédent, ce qui concentre la plupart des erreurs — sur un instrument liquide avec un spread d'un tick, la majorité des transactions consécutives s'impriment au même prix.

sign = np.sign(np.diff(prices, prepend=prices[0]))
sign[sign == 0] = np.nan
directions = pd.Series(sign).ffill().fillna(1).astype(int).values

Une implémentation de production avec la même sémantique de propagation est déjà disponible sous la forme de _tick_sign() dans les générateurs de barres d'imbalance de tick et de volume de au-delà des barres temporelles — réutilisez-la plutôt que de maintenir une seconde copie.

La règle des quotes

Transactions positionnées dans la géométrie bid-ask

Au lieu de comparer des transactions consécutives, comparez le prix de la transaction au midpoint courant 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

La règle des quotes utilise strictement plus d'information que le tick test, mais échoue ailleurs : elle ne dit rien des transactions qui s'impriment exactement au mid. Sur un instrument à spread étroit et contraint par le tick, ce n'est pas un cas limite — c'est une grande partie du flux. La règle a besoin d'un fallback, et c'est précisément le rôle de Lee-Ready.

Lee-Ready : l'hybride et son hypothèse de cotation obsolète

Flux de transactions et surface de quotes retardée

Lee et Ready (1991) ont combiné les deux : appliquer la règle des quotes et, pour les transactions au midpoint, revenir au tick test.

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

La partie la plus intéressante de l'article est celle qui est généralement reproduite sans sa justification. Lee et Ready n'ont pas seulement proposé un hybride ; ils ont proposé de comparer chaque transaction à la quote en vigueur cinq secondes plus tôt. Ce nombre n'est pas un choix de modélisation du marché. C'est une correction d'un artefact de diffusion : sur le consolidated tape du NYSE des années 1980, transactions et quotes étaient diffusées par des voies différentes avec des délais différents, et les quotes arrivaient généralement avant les transactions auxquelles elles correspondaient. Comparer une transaction à la quote imprimée au même instant revenait donc à la comparer à une quote qui l'intégrait déjà — inversant le signe précisément pour les transactions qui avaient fait bouger le marché.

Deux conséquences s'ensuivent pour quiconque transpose cela à la crypto.

La règle des 5 secondes est obsolète et son application est activement nuisible. Elle corrige un délai de diffusion qui n'existe plus. Les marchés actions ont résorbé l'écart au milieu des années 1990 ; les flux de transactions et de deltas du carnet d'une plateforme crypto partagent un espace de séquence et sont horodatés par le même moteur de matching à la résolution de la microseconde. Cinq secondes sur BTCUSDT ne sont pas une tolérance de synchronisation, mais des centaines de mises à jour du carnet. Retarder les quotes de cinq secondes ne corrige pas les quotes obsolètes, il les fabrique. Utilisez quote_lag=0 sauf si vous avez mesuré un décalage réel dans votre propre capture.

Mais le problème sous-jacent subsiste sous une nouvelle forme. L'obsolescence des quotes vient désormais de votre pipeline, et non de la plateforme. Reconstruire le carnet depuis un flux de deltas WebSocket et joindre les transactions par timestamp de temps réel introduit un retard dont le signe et l'ampleur dépendent de votre buffering, de votre politique de resynchronisation des snapshots et de votre horloge. Mesurez-le avant de faire confiance à un classifieur qui lit le carnet : rejouez une fenêtre, joignez les transactions à l'état du carnet et vérifiez quelle fraction tombe en dehors du [bid, ask] joint. Une transaction imprimée au-dessus de l'ask joint prouve que le carnet auquel vous l'avez associée était obsolète.

La correction diffère de celle de Lee et Ready ; le mode d'échec est identique.

Classification du volume agrégé

Clusters de volume par classe directionnelle

Easley, Lopez de Prado et O'Hara (2012) s'attaquent à un autre problème : que faire si vous ne voyez jamais les transactions individuelles ? BVC classe le volume agrégé d'une barre en le répartissant à l'aide de la CDF normale standard du changement de prix standardisé :

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}} et σΔP\sigma_{\Delta P} est l'écart-type des variations de prix de la barre.

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 est un outil différent, pas un outil moins bon, et trois propriétés l'expliquent. Il ne classe pas les transactions — il produit une fraction, de sorte qu'une barre qui clôture inchangée reçoit une répartition 50/50 plutôt qu'un pile ou face pour chaque transaction ; si votre consommateur a besoin d'un signe par transaction, BVC ne peut pas le fournir. Il fonctionne sur les barres plutôt que sur le flux, ce qui est précisément son intérêt : c'est la seule méthode présentée ici qui fonctionne lorsque les données au niveau des transactions n'existent pas. Enfin, σ\sigma est un paramètre libre qui déplace les résultats — une estimation sur tout l'échantillon introduit un lookahead, tandis qu'une estimation glissante courte fait saturer Φ\Phi et pousse chaque barre vers 0/100. Ce choix mérite une analyse de sensibilité, pas une valeur par défaut.

L'article original indique que BVC suit suffisamment bien la classification au niveau des transactions pour estimer VPIN. Les chiffres de précision dépendent fortement de la taille de la barre, de l'instrument et de l'estimateur de σ\sigma, et les nombres cités dans les sources secondaires mentionnent rarement les trois. Ne transposez pas un résultat d'une étude actions — mesurez-le.

La seule caractéristique propre à la classification

Signal de microstructure mis en évidence

Si vous entraînez un modèle pour effectuer cette tâche plutôt que d'exécuter une règle, la plupart des caractéristiques sont déjà publiées : le spread, le déséquilibre de profondeur, le déséquilibre du flux de transactions et la volatilité réalisée glissante sont présentés et implémentés dans la modélisation du spread avec le machine learning, et les caractéristiques multi-niveaux du carnet dans DeepLOB. Utilisez-les.

La caractéristique propre à ce problème, absente des deux articles, est la position de la transaction dans le spread :

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

Une transaction au bid correspond à 0, à l'ask à 1 et au mid à 0.5. Il s'agit de la généralisation continue de la règle des quotes : plutôt que d'appliquer un seuil au midpoint, elle fournit au modèle la distance, afin qu'il puisse apprendre qu'une transaction à 0.95 est presque certainement un achat tandis qu'une transaction à 0.55 est à peine informative. Les valeurs hors de [0,1][0, 1] sont le diagnostic de quote obsolète de la section précédente et méritent d'être enregistrées plutôt que tronquées.

Pour une baseline boostée sur ces caractéristiques, utilisez le pipeline déjà publié dans la modélisation du spread avec le machine learning en remplaçant l'objectif par un objectif binaire — notamment avec sa CV walk-forward purgée et sous embargo. N'évaluez pas un classifieur de microstructure avec un simple TimeSeriesSplit ; cet article et l'optimisation walk-forward expliquent tous deux pourquoi le nombre obtenu est optimiste. Pour les modèles séquentiels sur des snapshots du carnet, DeepLOB est l'architecture de référence et la section sur les considérations d'entraînement s'y applique sans changement.

Mesurer correctement : la crypto fournit les étiquettes

Flux de données de marché crypto étiqueté

Chaque chiffre de précision de la littérature sur la classification des transactions actions est estimé par rapport à un proxy de la vérité — TORQ, des sous-échantillons de journaux d'audit, les enregistrements internes d'une seule plateforme — car le véritable côté agressif n'a jamais été publié. La crypto le publie. isBuyerMaker dans les aggTrades de Binance est l'étiquette de chaque transaction, gratuitement. L'expérience est donc accessible à tous, et je ne l'ai pas trouvée publiée pour la crypto. Le protocole :

Données. Un mois d'aggTrades BTCUSDT avec le flux book ticker, joints de sorte que chaque transaction porte le bid et l'ask en vigueur juste avant elle. Répétez sur un altcoin peu liquide, puisque toute la question est de savoir ce qui se passe lorsque le spread est large.

Étiquette. direction = -1 if is_buyer_maker else +1. Ensuite, jetez l'indicateur et exécutez chaque méthode uniquement sur les données de prix et de quotes.

Méthodes. Tick test, règle des quotes, Lee-Ready avec quote_lag=0, Lee-Ready avec le retard historique de 5 secondes (pour quantifier les dégâts de la règle obsolète), et BVC à plusieurs tailles de barres comparé à la véritable fraction de volume acheteur par barre.

Ventilations. La précision globale est le nombre le moins intéressant. Ventilez par régime de spread (un tick contre large — la propagation du tick test devrait s'effondrer lorsque le spread est bloqué), selon que la transaction s'est imprimée au mid (pour isoler les cas où la règle des quotes s'abstient et où le fallback de Lee-Ready fait tout le travail), par taille de transaction (les entrées aggTrades sont des sweeps ; les grosses transactions déterminent l'impact et coûtent le plus lorsqu'elles sont mal signées), et par régime de volatilité (le σ\sigma de BVC est estimé, donc son erreur dépend par construction du régime).

Méthode Global Au mid Spread étroit Spread large Grosses transactions
Tick test
Règle des quotes
Lee-Ready (retard 0)
Lee-Ready (retard 5 s)
BVC (fraction par barre) n/a n/a

La précision n'est pas l'objectif

Frontière de classification et impact d'exécution

En reprenant la discipline de cadrage de DeepLOB : une précision de classification n'est pas un résultat, mais un élément qui sert à en produire un. Personne ne trade le signe d'une transaction — on trade quelque chose calculé à partir d'un flux complet de signes, et les erreurs ne s'y propagent pas de façon neutre. Un classifieur qui se trompe aléatoirement sur 10 % des transactions et un autre qui se trompe systématiquement sur les 10 % qui sont importantes et font bouger le prix produisent un flux d'ordres signé très différent à précision identique. L'échec de propagation du tick test est précisément corrélé aux prix plats — le régime où le flux signé est censé être informatif. C'est pourquoi les ventilations ci-dessus, et non le chiffre principal, constituent le véritable résultat.

Avec des étiquettes de vérité terrain, la question en aval est directement mesurable : estimez la quantité qui vous intéresse à partir des vrais signes, réestimez-la à partir des signes de chaque classifieur, puis publiez l'écart.

  • Impact du prix. Régressez les variations du mid-price sur le flux net signé des takers sur des fenêtres fixes — l'estimateur est déjà spécifié dans les modèles de coût du slippage et son implémentation fonctionnelle sur des données Binance publiques se trouve dans l'exécution optimale d'Almgren-Chriss. Ne le redéfinissez pas ici ; exécutez-le deux fois et comparez le coefficient ajusté.
  • Déséquilibre du flux d'ordres comme prédicteur. Calculez la corrélation entre OFI et les rendements futurs avec les vrais signes, puis avec les signes classifiés. La dégradation de cette corrélation est le coût de la mauvaise classification, exprimé dans la seule unité qui compte.
  • Métriques de toxicité. VPIN et les caractéristiques de déséquilibre du flux de transactions consomment du volume classifié ; les deux sont couverts dans la modélisation du spread avec le machine learning. Leur sensibilité à l'erreur de classification se mesure de la même manière.

Ce qu'il faut en retenir

Carte résolue du flux d'ordres

Si votre plateforme fournit un indicateur du côté agressif, utilisez-le. C'est le cas de la majeure partie de la crypto, la plupart du temps.

Si ce n'est pas le cas, Lee-Ready avec des quotes contemporaines est le bon choix par défaut — non parce qu'il est précis, mais parce qu'il est standard, et que la comparabilité avec trois décennies d'estimations publiées vaut mieux que quelques points de précision impossibles à vérifier. La règle des cinq secondes est un fossile de la latence du tape des années 1980 ; la reproduire sur des données modernes injecte précisément l'obsolescence qu'elle devait supprimer. Si vous n'avez que des barres, BVC est la seule option qui fonctionne vraiment, et elle répond à une autre question : une répartition du volume, pas le signe d'une transaction.

Dans tous les cas, mesurez par rapport aux étiquettes que la crypto fournit gratuitement, ventilez le résultat par régime de spread et taille de transaction, et indiquez le coût en aval de l'erreur plutôt que le pourcentage de classifications correctes.

blog.disclaimer

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

Gardez une longueur d'avance sur le marché

Abonnez-vous à notre newsletter pour des insights exclusifs sur le trading IA, des analyses de marché et des mises à jour de la plateforme.

Nous respectons votre vie privée. Désabonnement possible à tout moment.