Реализация защиты от Wash Trading в Matching Engine на Python

Реализация защиты от Wash Trading в Matching Engine на Python Python & API
Пошаговое руководство по реализации механизмов Self-Trade Prevention (STP) для предотвращения Wash Trading в собственном биржевом движке на Python.
Суть: Реализация Self-Trade Prevention (STP) в торговом движке предотвращает непреднамеренное или манипулятивное исполнение ордеров одного и того же участника рынка (Wash Trading). В статье представлен готовый код лимитного стакана на Python с поддержкой трех классических режимов STP: Cancel Newest, Cancel Oldest и Cancel Both.

Исходный код

При проектировании высоконагруженных торговых систем часто применяется gRPC на Python для передачи котировок между торговыми серверами, однако ядро системы — Matching Engine — должно содержать строгую бизнес-логику обработки ордеров. Одной из важнейших функций ядра является предотвращение самопересечения ордеров (Self-Trade Prevention, STP).

Для маркетмейкеров и алгоритмических трейдеров, реализующих сложные стратегии, такие как арбитраж ставок финансирования (Funding Arbitrage) на OKX, случайное совершение сделок с самим собой ведет к лишним расходам на комиссии и искажению торговой статистики. Анализ структуры ордеров и объемов часто требует расчета метрик вроде тех, что описаны в статье про расчет Taker Buy/Sell Ratio с Binance AggTrades на Python.

Ниже представлен исходный код класса MatchingEngine, реализующий полноценный стакан ордеров с логикой STP.

from enum import Enum
from dataclasses import dataclass
from typing import List, Dict, Tuple

class STPMode(Enum):
    CANCEL_NEWEST = 'CN'  # Отменить входящий ордер (Taker)
    CANCEL_OLDEST = 'CO'  # Отменить пассивный ордер в стакане (Maker)
    CANCEL_BOTH = 'CB'    # Отменить оба ордера

class Side(Enum):
    BUY = 'BUY'
    SELL = 'SELL'

@dataclass
class Order:
    order_id: str
    account_id: str
    side: Side
    price: float
    qty: float
    stp_mode: STPMode = STPMode.CANCEL_NEWEST

    def __repr__(self):
        return f"Order({self.order_id}, Acc:{self.account_id}, {self.side.value}, Px:{self.price}, Qty:{self.qty}, STP:{self.stp_mode.value})"

class MatchingEngine:
    def __init__(self):
        self.bids: List[Order] = []  # Покупатели (сортировка по убыванию цены)
        self.asks: List[Order] = []  # Продавцы (сортировка по возрастанию цены)
        self.trades: List[dict] = []

    def limit_order(self, incoming: Order) -> List[dict]:
        self.trades = []
        
        if incoming.side == Side.BUY:
            self._match(incoming, self.asks)
            if incoming.qty > 0 and incoming.order_id not in [o.order_id for o in self.bids]:
                # Если ордер не был полностью отменен по STP и остался объем
                self.bids.append(incoming)
                self.bids.sort(key=lambda x: x.price, reverse=True)
        else:
            self._match(incoming, self.bids)
            if incoming.qty > 0 and incoming.order_id not in [o.order_id for o in self.asks]:
                self.asks.append(incoming)
                self.asks.sort(key=lambda x: x.price)
        
        return self.trades

    def _match(self, incoming: Order, book_side: List[Order]):
        for maker in list(book_side):
            if incoming.qty <= 0:
                break
            
            # Проверка пересечения цен
            price_match = (incoming.side == Side.BUY and incoming.price >= maker.price) or \
                          (incoming.side == Side.SELL and incoming.price <= maker.price)
            
            if not price_match:
                break
            
            # Проверка на Wash Trading (Self-Trade)
            if incoming.account_id == maker.account_id:
                self._handle_stp(incoming, maker, book_side)
                if incoming.qty <= 0:
                    break
                continue  # Продолжаем сопоставление, если входящий ордер выжил
            
            # Обычное исполнение сделки
            match_qty = min(incoming.qty, maker.qty)
            match_price = maker.price
            
            self.trades.append({
                'maker_id': maker.order_id,
                'taker_id': incoming.order_id,
                'price': match_price,
                'qty': match_qty
            })
            
            incoming.qty -= match_qty
            maker.qty -= match_qty
            
            if maker.qty == 0:
                book_side.remove(maker)

    def _handle_stp(self, incoming: Order, maker: Order, book_side: List[Order]):
        mode = incoming.stp_mode
        print(f"[STP TRIGGERED] Mode: {mode.value} between Taker {incoming.order_id} and Maker {maker.order_id}")
        
        if mode == STPMode.CANCEL_NEWEST:
            # Отменяем входящий ордер полностью
            incoming.qty = 0
            
        elif mode == STPMode.CANCEL_OLDEST:
            # Отменяем пассивный ордер из стакана
            book_side.remove(maker)
            print(f"[STP] Maker order {maker.order_id} cancelled.")
            
        elif mode == STPMode.CANCEL_BOTH:
            # Отменяем оба ордера
            incoming.qty = 0
            book_side.remove(maker)
            print(f"[STP] Both orders {incoming.order_id} and {maker.order_id} cancelled.")

Разбор параметров

  • STPMode: Перечисление, определяющее поведение движка при обнаружении кросс-сделки. Поддерживает режимы CANCEL_NEWEST (отмена входящего), CANCEL_OLDEST (отмена пассивного) и CANCEL_BOTH (отмена обоих).
  • account_id: Уникальный идентификатор торгового счета. Сравнение этого поля у мейкера и тейкера является главным триггером для срабатывания защиты от самопересечения.
  • _match: Внутренний метод сведения ордеров, который проверяет ценовые условия и инициирует логику STP при совпадении account_id.
  • _handle_stp: Метод, инкапсулирующий логику модификации состояния ордеров и стакана в зависимости от выбранного режима предотвращения Wash Trading.

Как запустить

Для проверки работоспособности механизма защиты от Wash Trading вы можете запустить следующий тестовый сценарий, демонстрирующий работу всех трех режимов STP:

if __name__ == '__main__':
    engine = MatchingEngine()

    # Сценарий 1: CANCEL_NEWEST (По умолчанию)
    print("--- Сценарий 1: CANCEL_NEWEST ---")
    # Размещаем лимитный ордер на покупку от Account_1
    engine.limit_order(Order(order_id='O1', account_id='Account_1', side=Side.BUY, price=100.0, qty=10.0))
    # Пытаемся продать по той же цене от того же Account_1 с режимом CANCEL_NEWEST
    trades = engine.limit_order(Order(order_id='O2', account_id='Account_1', side=Side.SELL, price=100.0, qty=5.0, stp_mode=STPMode.CANCEL_NEWEST))
    print(f"Сделки: {trades}")
    print(f"Стакан BUY: {engine.bids}")
    print(f"Стакан SELL: {engine.asks}\n")

    # Очистим стаканы для следующего теста
    engine.bids.clear()
    engine.asks.clear()

    # Сценарий 2: CANCEL_OLDEST
    print("--- Сценарий 2: CANCEL_OLDEST ---")
    engine.limit_order(Order(order_id='O3', account_id='Account_1', side=Side.BUY, price=100.0, qty=10.0))
    # Входящий ордер имеет режим CANCEL_OLDEST
    trades = engine.limit_order(Order(order_id='O4', account_id='Account_1', side=Side.SELL, price=100.0, qty=5.0, stp_mode=STPMode.CANCEL_OLDEST))
    print(f"Сделки: {trades}")
    print(f"Стакан BUY: {engine.bids}")
    print(f"Стакан SELL: {engine.asks}\n")

    # Очистим стаканы
    engine.bids.clear()
    engine.asks.clear()

    # Сценарий 3: CANCEL_BOTH
    print("--- Сценарий 3: CANCEL_BOTH ---")
    engine.limit_order(Order(order_id='O5', account_id='Account_1', side=Side.BUY, price=100.0, qty=10.0))
    trades = engine.limit_order(Order(order_id='O6', account_id='Account_1', side=Side.SELL, price=100.0, qty=5.0, stp_mode=STPMode.CANCEL_BOTH))
    print(f"Сделки: {trades}")
    print(f"Стакан BUY: {engine.bids}")
    print(f"Стакан SELL: {engine.asks}")

Скопируйте весь код в один файл (например, matching_engine.py) и запустите его с помощью стандартного интерпретатора Python: python matching_engine.py. Вы увидите в консоли логи срабатывания STP и текущее состояние стакана после каждой операции.

Оцените статью
FinFluct