Исходный код
При проектировании высоконагруженных торговых систем часто применяется 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 и текущее состояние стакана после каждой операции.




