Торговля и «стакан»
Ордербук, matching и атомарные расчёты
Торговля и «стакан»
1) Режимы торгов
Платформа поддерживает вторичное обращение ЦФА. Это позволяет инвесторам продавать активы до срока погашения.
Типы заявок (Orders)
- Limit Order: Купить/продать по фиксированной цене (или лучше). Попадает в стакан.
- Market Order: Купить/продать немедленно по лучшей доступной цене.
2) Процесс матчинга (Matching Engine)
Схематичное представление того, как заявка превращается в сделку.
Loading diagram...
3) Атомарность (DvP - Delivery vs Payment)
Ключевой принцип безопасной торговли — Delivery versus Payment. Передача актива и оплата происходят одновременно.
Поток событий расчёта (Settlement Flow)
Loading diagram...
Модель проводок
При совершении сделки Ledger-DB выполняет следующие записи:
| Субсчет | Дебет | Кредит | Описание |
|---|---|---|---|
| User A (Buyer) | -5000 RUB | Списание средств за покупку | |
| User B (Seller) | +4950 RUB | Зачисление средств за продажу | |
| System Fees | +50 RUB | Комиссия платформы (1%) | |
| User B (Seller) | -10 CFA | Списание актива | |
| User A (Buyer) | +10 CFA | Зачисление актива покупателю |
4) Контроль и Ограничения
- Режим работы: Торги могут быть приостановлены (Trading Halt) администратором или автоматически при высокой волатильности.
- Манипулирование: Anti-Fraud модуль анализирует сделки на предмет Wash Trading (торговля с самим собой) и других аномалий.
5) Размещение ордера
Create Order
const order = await marketService.placeOrder(
{ requestId, correlationId, idempotencyKey },
{
userId: 'user-123',
instrumentId: 'instrument-456',
side: OrderSide.BUY,
type: OrderType.LIMIT,
price: { amount: '100', currencyCode: 'RUB' },
quantity: 10,
timeInForce: TimeInForce.GTC,
},
);Проверки:
- KYC статус пользователя
- Лимиты торговли
- Баланс (для покупки)
- Количество актива (для продажи)
Cancel Order
const cancelled = await marketService.cancelOrder(
{ requestId, correlationId, idempotencyKey },
'order-789',
);6) Исполнение сделки
DvP Settlement
Атомарный расчет с гарантией поставки против платежа.
Проводки:
| Субсчет | Дебет | Кредит | Описание |
|---|---|---|---|
| User A (Buyer) | -5000 RUB | Списание средств за покупку | |
| User B (Seller) | +4950 RUB | Зачисление средств за продажу | |
| System Fees | +50 RUB | Комиссия платформы (1%) | |
| User B (Seller) | -10 CFA | Списание актива | |
| User A (Buyer) | +10 CFA | Зачисление актива покупателю |
Процесс:
Loading diagram...
7) Order Book
Структура
class OrderBook {
private bids: PriorityQueue<Order> = new PriorityQueue(
(a, b) => b.price - a.price, // Высокая цена в начале
);
private asks: PriorityQueue<Order> = new PriorityQueue(
(a, b) => a.price - b.price, // Низкая цена в начале
);
addOrder(order: Order): void {
if (order.side === OrderSide.BUY) {
this.bids.push(order);
} else {
this.asks.push(order);
}
}
bestBid(): Order | null {
return this.bids.peek();
}
bestAsk(): Order | null {
return this.asks.peek();
}
getSnapshot(): OrderBookSnapshot {
return {
instrumentId: this.instrumentId,
bids: this.bids.toArray().map((o) => ({
price: o.price,
quantity: o.quantity,
})),
asks: this.asks.toArray().map((o) => ({
price: o.price,
quantity: o.quantity,
})),
capturedAt: new Date(),
};
}
}Хранение в Redis
class OrderBookStorage {
private redis: Redis;
async saveSnapshot(instrumentId: string, snapshot: OrderBookSnapshot): Promise<void> {
const key = `orderbook:${instrumentId}`;
await this.redis.setex(key, 3600, JSON.stringify(snapshot));
}
async getSnapshot(instrumentId: string): Promise<OrderBookSnapshot | null> {
const key = `orderbook:${instrumentId}`;
const data = await this.redis.get(key);
return data ? JSON.parse(data) : null;
}
async publishUpdate(instrumentId: string, snapshot: OrderBookSnapshot): Promise<void> {
await this.redis.publish(`orderbook:${instrumentId}`, JSON.stringify(snapshot));
}
}8) WebSocket Updates
Подписка на стакан
// Клиент подписывается на обновления стакана
const ws = new WebSocket('wss://api.maniton.io/orderbook?instrument_id=instrument-456');
ws.onmessage = (event) => {
const snapshot = JSON.parse(event.data);
updateOrderBook(snapshot);
};Публикация обновлений
// Сервис публикует обновления стакана
await this.orderBookStorage.publishUpdate(instrumentId, snapshot);9) Мониторинг
Метрики
export const tradingMetrics = {
ordersPlacedTotal: new Counter({
name: 'orders_placed_total',
help: 'Total number of orders placed',
labelNames: ['instrument_id', 'side'],
}),
tradesExecutedTotal: new Counter({
name: 'trades_executed_total',
help: 'Total number of trades executed',
labelNames: ['instrument_id'],
}),
orderBookDepth: new Gauge({
name: 'order_book_depth',
help: 'Order book depth by instrument',
labelNames: ['instrument_id', 'side'],
}),
tradeExecutionDuration: new Histogram({
name: 'trade_execution_duration_seconds',
help: 'Time spent executing trades',
buckets: [0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5],
}),
};Логи
this.logger.log('Order placed', {
orderId: order.orderId,
userId: order.userId,
instrumentId: order.instrumentId,
side: order.side,
price: order.price,
quantity: order.quantity,
});
this.logger.log('Trade executed', {
tradeId: trade.tradeId,
buyerId: trade.buyerId,
sellerId: trade.sellerId,
instrumentId: trade.instrumentId,
quantity: trade.quantity,
price: trade.price,
fee: trade.fee,
});10) Troubleshooting
Проблема: Ордер не исполняется
Диагностика:
# Проверка стакана
curl http://market-service:3004/orderbook?instrument_id=instrument-456
# Проверка ордера
curl http://market-service:3004/orders/$ORDER_ID
# Проверка холдов
curl http://ledger-service:3003/holdsРешение:
- Проверьте, что цена соответствует стакану
- Проверьте, что холды созданы
- Проверьте, что баланс достаточен
Проблема: Сделка не проходит
Диагностика:
# Проверка статуса сделки
curl http://market-service:3004/trades/$TRADE_ID
# Проверка операции в Ledger
curl http://ledger-service:3003/operations/$OPERATION_ID
# Проверка транзакцию в блокчейне
curl -X POST http://besu-connector:3004/getTransactionReceipt \
-H "Content-Type: application/json" \
-d '{"tx_hash": "0x..."}'Решение:
- Проверьте, что транзакция прошла в блокчейне
- Проверьте, что холды потреблены
- Ручной разбор если нужно