Если вы до сих пор думаете, что сделка на бирже - это просто клик по кнопке «Buy», за которым происходит магия, у меня для вас новости. На уровне микросекунд и миллисекунд рынок - это жесткое поле боя, где ликвидность переливается из одного состояния в другое, а скорость исполнения и анатомия пула определяют, кто получит профит, а кто заплатят за проскальзывание (slippage) и токсичный поток.
В централизованном трейдинге (TradFi и CEX) правит Order Book. В DeFi базовым стандартом стал AMM. Но когда мы спускаемся на высокочастотный уровень (HFT), разница между ними оказывается не просто технологической, она фундаментально меняет механику ценообразования, арбитража и исполнения ордеров.
Давайте разберем, как устроена механика ликвидности под капотом обеих систем, где кроются неявные издержки и как использовать эти знания на практике.
1. Анатомия L2/L3 стакана: Как на самом деле выглядит ликвидность
Большинство розничных трейдеров видят стакан L2: агрегированные объемы на каждом ценовом уровне. Но HFT-алгоритмы и институционалы работают с L3 (Order-by-Order) - сырым потоком данных, где каждое событие имеет свой уникальный идентификатор (order_id), точный таймштамп и позицию в очереди.

Принцип Price-Time Priority (FIFO)
В классическом matching engine ордера исполняются строго по приоритету:
- Цена (лучшая цена получает приоритет).
- Время (при равной цене первым исполняется тот, кто встал в очередь раньше).
Из-за очереди по времени возникают два ключевых эффекта микроструктуры:
Adverse Selection (Неблагоприятный выбор): Если ваш лимитный ордер на покупку быстро исполняется, велика вероятность, что на рынок пришел крупный продавец с агрессивным потоком (toxic flow), и цена продолжит падать дальше.
Queue Position Value: Место в начале очереди ценового уровня имеет самостоятельную стоимость. Высокочастотные роботы постоянно отменяют и перевыставляют ордера (механика Spoofing/Layering или адаптивное управление ликвидностью), чтобы удерживать приоритет, не принимая на себя лишний инвентарный риск.
2. AMM: От классического Constant Product к концентрированной ликвидности
AMM принципиально отличается от стакана: здесь нет очереди ордеров. Цена определяется математической функцией инварианта.
Constant Product (v2): x · y = k
В классическом Uniswap v2 ликвидность распределена от нуля до бесконечности.
При совершении сделки на объем Δx трейдер забирает из пула Δy, вычисляемый с учетом комиссии γ = (1 - fee):
Δy = (y · Δx · γ) / (x + Δx · γ)
Цена исполнения не фиксирована, она «скользит» вдоль кривой в момент исполнения.
Concentrated Liquidity (v3 / Tick-based)
Uniswap v3 превратил AMM в подобие стакана, разрешив поставщикам ликвидности (LP) задавать диапазоны [Pa, Pb].
Цена разбита на тики (ticks), где каждый тик соответствует изменению цены на 0.01% (1 bps):
P(i) = 1.0001i
Ликвидность L внутри диапазона работает как «виртуальный» инвариант, но при пересечении границы тика активная ликвидность резко меняется. Для высокочастотного арбитражера это означает следующее: стакан AMM v3 дискретен, и его глубина меняется ступенчато при пробитии тиков.
3. Сравнительный анализ: Microstructure Matrix
| Параметр | Order Book (CEX/TradFi) | AMM (Uniswap v2) | Concentrated AMM (v3) |
|---|---|---|---|
| Приоритет исполнения | Price-Time (FIFO) / Pro-Rata | Gas Price / MEV (Priority Fee) | Gas Price / MEV (Priority Fee) |
| Задержка (Latency) | Микросекунды / Наносекунды | Время блока (или slotted latency) | Время блока / P2P-мемпул |
| Формирование цены | Непрерывный аукцион (Continuous Double Auction) | Динамически от объёма сделки (x · y = k) | Динамически в рамках активного тика |
| Издержки ликвидности | Непостоянный убыток отсутствует (но есть Inventory Risk) | Impermanent Loss (IL) | Loss-Versus-Rebalancing (LVR) |
| Прозрачность потока | L3 Data Feeds (Pitch, ITCH, FIX) | Mempool (до блока) / On-chain (после) | Mempool (до блока) / On-chain (после) |
4. Скрытые механики: MEV, LVR и Toxic Flow
В стакане CEX высокочастотник сражается за наносекунды физической сетевой задержки (colocation в дата-центре биржи, оптоволокно, FPGA-платы).
В DeFi понятие времени искривляется: фундаментальную роль играет структурирование блока.
MEV (Maximal Extractable Value)
В AMM порядок транзакций внутри блока формируется поисковиками (Searchers) и строителями блоков (Builders). Это порождает специфические HFT-механики:
- Front-running / Back-running: Перехват сделок в мемпуле путем предложения более высокой комиссии (Priority Fee).
- Sandwich Attacks: Покупка перед крупной пользовательской транзакцией для завышения цены и немедленная продажа сразу после нее.
LVR (Loss-Versus-Rebalancing)
Все привыкли считать риски LP через Impermanent Loss (IL). Но на высокочастотном уровне актуальнее метрика LVR, предложенная исследователями из Колумбийского университета и Paradigm.
LVR (Loss-Versus-Rebalancing) — это систематический убыток поставщика ликвидности в AMM по сравнению с тем же портфелем, постоянно ребалансируемым на CEX с использованием внешних цен.
Причина LVR - арбитраж волатильности. Когда цена на CEX уходит вперед, AMM-пул оставляет устаревшую цену (stale price). Высокочастотный арбитражер видит этот спред, забирает дешевую ликвидность из AMM и перекрывает ее на CEX. Поставщик ликвидности в AMM всегда исполняется по худшей цене против информированного потока (toxic flow).
LVR ≈ (σ² / 8) · ∫0T St · Lt dt
Где σ — волатильность актива, St — цена, Lt — активная ликвидность. Вывод: Чем выше волатильность рынка, тем больше денег LP отдают HFT-арбитражерам.
5. Практика: Python-скрипт расчета котировки AMM v3 vs L2 Depth
Перейдем к коду. Напишем рабочий инструмент для расчета реальной цены исполнения (с учетом проскальзывания) для условного ордера, сравнивая глубину L2-стакана и пул Constant Product AMM.
Код написан на чистом Python без тяжелых сторонних фреймворков.
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import List
class FeeMode(Enum):
QUOTE = "quote" # комиссия удерживается из входящего USDT
BASE = "base" # комиссия удерживается из полученного ETH
@dataclass
class AskLevel:
price: float
volume: float
class OrderBookL2:
"""
L2 стакан (Ask сторона).
Пример пары ETH/USDT:
price = USDT за 1 ETH
volume = ETH
"""
def __init__(
self,
asks: List[tuple[float, float]],
taker_fee: float = 0.001,
) -> None:
if not (0 <= taker_fee < 1):
raise ValueError("taker_fee должен быть в диапазоне [0, 1)")
self.taker_fee = float(taker_fee)
self.asks: List[AskLevel] = []
for price, volume in asks:
if price <= 0:
raise ValueError("Цена должна быть > 0")
if volume <= 0:
raise ValueError("Объем должен быть > 0")
self.asks.append(
AskLevel(
price=float(price),
volume=float(volume),
)
)
self.asks.sort(key=lambda x: x.price)
def best_ask(self) -> float:
if not self.asks:
raise ValueError("Стакан пуст")
return self.asks[0].price
def spot_price(self) -> float:
return self.best_ask()
def total_liquidity_quote(self) -> float:
return sum(
level.price * level.volume
for level in self.asks
)
def total_liquidity_base(self) -> float:
return sum(
level.volume
for level in self.asks
)
def simulate_buy(
self,
amount_in_quote: float,
mutate: bool = True,
fee_mode: FeeMode = FeeMode.QUOTE,
) -> tuple[float, float]:
"""
Покупка базового актива за amount_in_quote.
Возвращает:
(
получено_базового_актива,
средняя_эффективная_цена
)
"""
if amount_in_quote <= 0:
raise ValueError(
"amount_in_quote должен быть > 0"
)
if fee_mode == FeeMode.QUOTE:
quote_for_trade = (
amount_in_quote *
(1.0 - self.taker_fee)
)
else:
quote_for_trade = amount_in_quote
remaining_quote = quote_for_trade
total_base_bought = 0.0
new_levels: List[AskLevel] = []
for level in self.asks:
if remaining_quote <= 0:
new_levels.append(level)
continue
level_cost = (
level.price *
level.volume
)
if remaining_quote >= level_cost:
total_base_bought += level.volume
remaining_quote -= level_cost
else:
base_from_level = (
remaining_quote /
level.price
)
total_base_bought += base_from_level
remaining_volume = (
level.volume -
base_from_level
)
if remaining_volume > 1e-12:
new_levels.append(
AskLevel(
level.price,
remaining_volume,
)
)
remaining_quote = 0.0
if remaining_quote > 1e-9:
raise ValueError(
"Недостаточно ликвидности в стакане"
)
if fee_mode == FeeMode.BASE:
total_base_bought *= (
1.0 - self.taker_fee
)
if total_base_bought <= 0:
raise ValueError(
"Получен нулевой объем"
)
avg_price = (
amount_in_quote /
total_base_bought
)
if mutate:
self.asks = new_levels
return total_base_bought, avg_price
class ConstantProductAMM:
"""
Uniswap V2 style AMM
x = ETH
y = USDT
spot = y / x
"""
def __init__(
self,
reserve_x: float,
reserve_y: float,
fee: float = 0.003,
) -> None:
if reserve_x <= 0:
raise ValueError(
"reserve_x должен быть > 0"
)
if reserve_y <= 0:
raise ValueError(
"reserve_y должен быть > 0"
)
if not (0 <= fee < 1):
raise ValueError(
"fee должен быть в диапазоне [0, 1)"
)
self.x = float(reserve_x)
self.y = float(reserve_y)
self.fee = float(fee)
def spot_price(self) -> float:
return self.y / self.x
def invariant(self) -> float:
return self.x * self.y
def simulate_buy(
self,
amount_y_in: float,
mutate: bool = True,
) -> tuple[float, float]:
"""
Покупка ETH за USDT.
Возвращает:
(
получено_ETH,
средняя_цена
)
"""
if amount_y_in <= 0:
raise ValueError(
"amount_y_in должен быть > 0"
)
y_effective = (
amount_y_in *
(1.0 - self.fee)
)
x_out = (
self.x *
y_effective
) / (
self.y +
y_effective
)
if x_out <= 0:
raise ValueError(
"Получен нулевой объем"
)
if x_out >= self.x:
raise ValueError(
"Недостаточно ликвидности пула"
)
avg_price = (
amount_y_in /
x_out
)
if mutate:
self.x -= x_out
self.y += amount_y_in
return x_out, avg_price
def print_trade_result(
title: str,
received: float,
avg_price: float,
spot_price: float,
) -> None:
slippage = (
(avg_price - spot_price)
/ spot_price
) * 100
print(
f"{title:<12}"
f" Получено: {received:.6f}"
f" | Средняя цена: {avg_price:,.4f}"
f" | Проскальзывание: {slippage:.4f}%"
)
def main() -> None:
asks = [
(3000.0, 1.5),
(3001.0, 2.0),
(3002.5, 5.0),
(3005.0, 10.0),
(3010.0, 25.0),
]
orderbook = OrderBookL2(
asks=asks,
taker_fee=0.001,
)
amm = ConstantProductAMM(
reserve_x=1000.0,
reserve_y=3_000_000.0,
fee=0.003,
)
trade_size = 15_000.0
ob_spot = orderbook.spot_price()
amm_spot = amm.spot_price()
ob_received, ob_avg = orderbook.simulate_buy(
amount_in_quote=trade_size,
mutate=True,
fee_mode=FeeMode.QUOTE,
)
amm_received, amm_avg = amm.simulate_buy(
amount_y_in=trade_size,
mutate=True,
)
print(
f"\n=== Сравнение исполнения заявки "
f"на {trade_size:,.2f} USDT ===\n"
)
print_trade_result(
"ORDERBOOK",
ob_received,
ob_avg,
ob_spot,
)
print_trade_result(
"AMM V2",
amm_received,
amm_avg,
amm_spot,
)
print("\n=== Состояние после сделки ===\n")
if orderbook.asks:
print(
f"Best Ask: "
f"{orderbook.best_ask():,.4f}"
)
else:
print("Стакан полностью выкуплен")
print(
f"AMM Spot: "
f"{amm.spot_price():,.4f}"
)
print(
f"AMM Reserves: "
f"{amm.x:.6f} ETH | "
f"{amm.y:.2f} USDT"
)
if __name__ == "__main__":
main()Как трактовать результаты
В глубоких пулах AMM малые ордера исполняются с минимальным проскальзыванием, но при росте объема цена в AMM растет по выпуклой кривой. В стакане же все зависит от «стенок» (liquidity walls). Если в стакане тонко, вы пробьете несколько уровней и получите разнос по цене куда хуже, чем в AMM.
6. Гибридные модели: Как DEX внедряют Order Book и почему CEX смотрят на AMM
Граница между стаканами и AMM постепенно размывается. Высокочастотные трейдеры и институционалы быстро уперлись в потолок возможностей классического Uniswap v2: низкая капиталоэффективность и невозможность выставлять сложные лимитные ордера (Stop-loss, Take-profit, Iceberg) делали DEX неудобными для активного market making.
Это привело к эволюционному сдвигу и появлению гибридных архитектур.
1. On-Chain и L2 Order Books (dYdX, Hyperliquid, Vertex)
С появлением высокопроизводительных L1/L2 сетей (Aptos, Sui, Arbitrum) и специализированных AppChain стал возможен запуск полноценных стаканов прямо в блокчейне.
Off-chain matching + On-chain settlement: Модули сведения ордеров работают на ультра-быстрых движках вне сети (для достижения задержки < 10 мс), а результаты исполнения и маржинальные требования фиксируются в смарт-контрактах.
Fully On-chain (Hyperliquid, Serum/Ellipsis): Стакан полностью живёт в консенсусе блокчейна. Главный вызов здесь - обработка фатального объема операций: замена и отмена ордеров (Cancel/Replace) в HFT составляет до 95–98% от всех входящих транзакций.
2. Intent-Based архитектура и RFQ (Request for Quote)
Модель "Intents" (Uniswap X, 1inch Fusion, CoW Protocol) фактически возвращает институциональный подход к исполнению.
Трейдер подписывает не транзакцию взаимодействия с конкретным пулом, а намерение (например: «Хочу обменять 100 ETH на не менее чем 300,000 USDC»). Далее сеть сторонних исполнителей (Fillers / Solvers) соревнуется за право исполнить этот ордер, используя любые источники ликвидности — CEX, private market maker inventory или DEX.

Для практикующего трейдера это означает нулевой slippage и защиту от MEV-сэндвичей: весь риск проскальзывания и исполнения берет на себя Solver, зарабатывая на межактивном арбитраже.
7. Практика: Продвинутый скрипт арбитража между CEX L2 Order Book и AMM v2
На практике HFT-боты постоянно ищут ценовые дисбалансы между CEX и AMM. Если на CEX цена актива взлетает из-за рыночной покупки, пул AMM на некоторое время остается «устаревшим» (stale). Арбитражер покупкой в AMM сдвигает его цену до уровня CEX, продавая купленный объем обратно на CEX.
Ниже представлен рабочий Python-скрипт, который в реальном времени рассчитывает оптимальный размер арбитражной сделки (dx) с учетом комиссии AMM, глубины стакана CEX и комиссии мейкера/тейкера.
from __future__ import annotations
from dataclasses import dataclass
from typing import List, Dict, Any
@dataclass
class BidLevel:
price: float
volume: float
class OrderBookSide:
"""
Bid сторона стакана.
price = цена покупки
volume = объем базового актива
"""
def __init__(self, bids: List[tuple[float, float]]):
self.bids: List[BidLevel] = []
for price, volume in bids:
if price <= 0:
raise ValueError("Цена должна быть > 0")
if volume <= 0:
raise ValueError("Объем должен быть > 0")
self.bids.append(
BidLevel(
float(price),
float(volume)
)
)
self.bids.sort(
key=lambda x: x.price,
reverse=True
)
def is_empty(self) -> bool:
return len(self.bids) == 0
def best_bid(self) -> float:
if self.is_empty():
raise ValueError("Стакан пуст")
return self.bids[0].price
def cumulative_levels(self) -> List[tuple[float, float]]:
"""
Возвращает точки изменения ликвидности.
[
(2 ETH, VWAP до 2 ETH),
(7 ETH, VWAP до 7 ETH),
...
]
"""
result = []
cumulative_x = 0.0
cumulative_y = 0.0
for level in self.bids:
cumulative_x += level.volume
cumulative_y += level.volume * level.price
result.append(
(
cumulative_x,
cumulative_y
)
)
return result
def execute_sell(
self,
amount_x: float
) -> tuple[float, float]:
"""
Продажа amount_x в стакан.
Возвращает:
(
получено_USDT,
VWAP
)
"""
if amount_x <= 0:
raise ValueError(
"amount_x должен быть > 0"
)
remaining_x = amount_x
received_y = 0.0
for level in self.bids:
if remaining_x <= 0:
break
executed = min(
remaining_x,
level.volume
)
received_y += (
executed *
level.price
)
remaining_x -= executed
if remaining_x > 1e-9:
raise ValueError(
"Недостаточно ликвидности стакана"
)
vwap = received_y / amount_x
return received_y, vwap
class AMMPool:
"""
Constant Product AMM
x = ETH
y = USDT
"""
def __init__(
self,
reserve_x: float,
reserve_y: float,
fee: float = 0.003
):
if reserve_x <= 0:
raise ValueError(
"reserve_x должен быть > 0"
)
if reserve_y <= 0:
raise ValueError(
"reserve_y должен быть > 0"
)
if not (0 <= fee < 1):
raise ValueError(
"fee должен быть в диапазоне [0,1)"
)
self.x = float(reserve_x)
self.y = float(reserve_y)
self.fee = float(fee)
def spot_price(self) -> float:
return self.y / self.x
def calculate_out_x(
self,
amount_y_in: float
) -> float:
if amount_y_in <= 0:
raise ValueError(
"amount_y_in должен быть > 0"
)
y_effective = (
amount_y_in *
(1.0 - self.fee)
)
x_out = (
self.x *
y_effective
) / (
self.y +
y_effective
)
return x_out
def calculate_required_y_for_x(
self,
target_x_out: float
) -> float:
"""
Инверсия формулы AMM.
Сколько USDT нужно внести,
чтобы получить target_x_out.
"""
if target_x_out <= 0:
raise ValueError(
"target_x_out должен быть > 0"
)
if target_x_out >= self.x:
raise ValueError(
"Нельзя вывести весь резерв"
)
y_effective = (
target_x_out *
self.y
) / (
self.x -
target_x_out
)
amount_y_in = (
y_effective /
(1.0 - self.fee)
)
return amount_y_in
def find_optimal_arbitrage(
amm: AMMPool,
cex_bids: OrderBookSide,
cex_fee: float = 0.001
) -> Dict[str, Any]:
if not (0 <= cex_fee < 1):
raise ValueError(
"cex_fee должен быть в диапазоне [0,1)"
)
if cex_bids.is_empty():
return {
"opportunity": False,
"reason": "Пустой стакан CEX"
}
best_bid = cex_bids.best_bid()
if (
best_bid *
(1.0 - cex_fee)
<=
amm.spot_price()
):
return {
"opportunity": False,
"reason": "Спред отсутствует"
}
best_result = None
best_profit = float("-inf")
cumulative = cex_bids.cumulative_levels()
for cumulative_x, cumulative_y in cumulative:
try:
required_usdt = (
amm.calculate_required_y_for_x(
cumulative_x
)
)
gross_usdt = cumulative_y
net_usdt = (
gross_usdt *
(1.0 - cex_fee)
)
profit = (
net_usdt -
required_usdt
)
if profit > best_profit:
avg_buy_price = (
required_usdt /
cumulative_x
)
avg_sell_price = (
gross_usdt /
cumulative_x
)
amm_spot = (
amm.spot_price()
)
impact_pct = (
(
avg_buy_price -
amm_spot
)
/
amm_spot
) * 100
roi_pct = (
profit /
required_usdt
) * 100
best_profit = profit
best_result = {
"opportunity": True,
"input_usdt_amm": required_usdt,
"bought_x": cumulative_x,
"gross_usdt_cex": gross_usdt,
"net_usdt_cex": net_usdt,
"net_profit_usdt": profit,
"roi_pct": roi_pct,
"amm_spot_price": amm_spot,
"amm_avg_buy_price": avg_buy_price,
"amm_price_impact_pct": impact_pct,
"cex_vwap": avg_sell_price,
}
except ValueError:
continue
if best_result is None:
return {
"opportunity": False,
"reason": "Ликвидность недостаточна"
}
if best_result["net_profit_usdt"] <= 0:
return {
"opportunity": False,
"reason": "Проскальзывание уничтожает прибыль"
}
return best_result
def main():
amm = AMMPool(
reserve_x=1000.0,
reserve_y=3_000_000.0,
fee=0.003
)
cex = OrderBookSide([
(3050.0, 2.0),
(3048.0, 5.0),
(3045.0, 10.0),
(3040.0, 20.0),
(3030.0, 50.0),
])
result = find_optimal_arbitrage(
amm,
cex,
cex_fee=0.0005
)
print(
"\n=== Результат поиска арбитража ===\n"
)
if not result["opportunity"]:
print(
f"Статус: ОТМЕНА\n"
f"Причина: {result['reason']}"
)
return
print("Статус: НАЙДЕНА СВЯЗКА")
print()
print(
f"Вход в AMM: "
f"${result['input_usdt_amm']:,.2f}"
)
print(
f"Куплено ETH: "
f"{result['bought_x']:.6f}"
)
print(
f"Выручка CEX: "
f"${result['gross_usdt_cex']:,.2f}"
)
print(
f"Чистая выручка CEX: "
f"${result['net_usdt_cex']:,.2f}"
)
print(
f"Чистый профит: "
f"${result['net_profit_usdt']:,.2f}"
)
print(
f"ROI: "
f"{result['roi_pct']:.4f}%"
)
print(
f"AMM Spot: "
f"{result['amm_spot_price']:.4f}"
)
print(
f"AMM Avg Buy: "
f"{result['amm_avg_buy_price']:.4f}"
)
print(
f"AMM Impact: "
f"{result['amm_price_impact_pct']:.4f}%"
)
print(
f"CEX VWAP: "
f"{result['cex_vwap']:.4f}"
)
if __name__ == "__main__":
main()8. Прикладные выводы для трейдера: Как не платить «налог на неведение»
Понимание микроструктуры - это не просто теоретическая академическая база. Это инструмент, спасающий депозит от скрытых утечек капитала.
- Контролируйте тип ордера под задачи рынка: На CEX покупка «по рынку» (Market Order) большого объема гарантированно исполняет вас об несколько уровней стакана, давая отрицательное проскальзывание. Если вам нужна срочность, используйте ордера IOC (Immediate-or-Cancel) или FOK (Fill-or-Kill) с явным указанием худшей приемлемой цены.
- Считайте эффективную глубину, а не спред: Узкий спред на L2 ($0.01) часто бывает иллюзией. Если на лучшем Bid/Ask стоит объем в $100, ваш ордер на $50,000 «прошьет» стакан до глубин с совершенно другой ценой.
- Учитывайте токсичность потока в AMM: Если вы предоставляете ликвидность в Uniswap v3 в узком диапазоне во время выхода важных новостей или высокой волатильности, вы становитесь мишенью для HFT-арбитражера. Метрика LVR сожрет всю доходность от комиссий за считанные минуты.
- Защищайтесь от MEV на DEX: При сделках в AMM всегда ограничивайте параметр Slippage Tolerance (ставите не более 0.1–0.3%) или используйте частные RPC-узлы (например, Flashbots Protect / MEV-Blocker), чтобы ваши транзакции не попадали в публичный мемпул для сэндвич-атак.
Делитесь в комментариях, с какими объемами проскальзывания вы чаще всего сталкиваетесь при исполнении крупных ордеров и какую модель (CEX Order Book или DEX AMM) предпочитаете для повседневных задач!