Дисклеймер: Информация в этой статье предоставлена исключительно в образовательных и ознакомительных целях и не является финансовым, инвестиционным или торговым советом. Торговля криптовалютами сопряжена с высоким риском убытков.
Представьте: вы стоите на оживлённом восточном базаре. У одного менялы доллар стоит 90 рублей, у второго — 92. Третий предлагает евро за доллары по 0.91, а четвёртый — рубли за евро по 102. Если быстро пробежать по кругу, обменивая валюты, можно вернуться к рублям с прибылью. Теперь увеличьте масштаб: 500 криптовалют, 30 бирж, тысячи торговых пар — спот, бессрочные фьючерсы, поставочные контракты, опционы. Как найти прибыльный маршрут в этом лабиринте? Ответ скрывается в теории графов — той самой, которую Эйлер придумал, решая задачу о Кёнигсбергских мостах три века назад.
Эта статья — первая часть серии «Сложные цепочки арбитража между фьючерсами и спотом». Мы разберём, как превратить хаотичный рынок в математическую структуру, где арбитражные возможности становятся буквально видимыми. И покажем, как современные алгоритмы — от классического Bellman-Ford до революционного RICH (VLDB 2025) — позволяют находить эти возможности быстрее, чем они успевают исчезнуть.
Ключевая идея: арбитраж как отрицательный цикл
Начнём с фундамента. Арбитражная возможность существует, когда последовательность обменов, начинающаяся и заканчивающаяся одним и тем же активом, приносит прибыль. Математически это выражается просто: если у нас есть цепочка обменных курсов r1, r2, ..., rk, арбитраж существует, когда:
r1 * r2 * ... * rk > 1
Проблема в том, что большинство графовых алгоритмов работают с суммами весов, а не с произведениями. Здесь на помощь приходит элегантное логарифмическое преобразование:
weight(A -> B) = -log(rate(A -> B))
Что это даёт? После преобразования:
Сумма весов вдоль пути эквивалентна произведению курсов
Отрицательный цикл (сумма весов < 0) соответствует прибыльному арбитражу (произведение курсов > 1)
Стандартные алгоритмы кратчайших путей теперь напрямую обнаруживают арбитраж
Давайте разберём это на пальцах. Если BTC/USDT = 50000 (мы можем продать 1 BTC за 50000 USDT), то вес ребра BTC -> USDT равен -log(50000) ≈ -10.82. Если ETH/BTC = 0.06, вес ребра ETH -> BTC равен -log(0.06) ≈ 2.81. Обратно: USDT -> ETH по курсу 1/3001, вес равен -log(1/3001) ≈ 8.01. Сумма по циклу: -10.82 + 8.01 + 2.81 = 0.0 — нет арбитража. Но стоит ценам чуть разойтись — и сумма станет отрицательной, сигнализируя о прибыльной возможности.
Криптовалютный рынок как ориентированный взвешенный граф: вершины — активы, рёбра — обменные курсы в логарифмическом пространстве. Отрицательный цикл (выделен красным) = арбитраж.
Учёт комиссий
Комиссии уменьшают эффективный обменный курс. С комиссией f вес ребра становится:
weight(A -> B) = -log(rate(A -> B) * (1 - f))
= -log(rate(A -> B)) - log(1 - f)
Поскольку log(1 - f) отрицателен, комиссии добавляют положительный вес к рёбрам, делая отрицательные циклы труднее находимыми. Это корректно отражает реальность: комиссии снижают прибыльность арбитража. Для трёхногой сделки с комиссией 0.1% на каждом шагу нужно, чтобы суммарная ценовая дискрепанция превышала 0.3% — и это ещё без учёта проскальзывания.
Bellman-Ford: классика, проверенная временем
Алгоритм Bellman-Ford — основа основ для обнаружения арбитража. Он вычисляет кратчайшие пути от одного источника ко всем остальным вершинам во взвешенном ориентированном графе. Критически важное свойство: он может работать с отрицательными весами рёбер и детектирует отрицательные циклы.
Сложность: O(|V| * |E|) по времени, O(|V|) по памяти.
Алгоритм работает в два этапа. Первый — |V|-1 итераций релаксации: для каждого ребра (u, v) с весом w проверяем, можно ли улучшить расстояние до v, пройдя через u. Второй — этап детектирования: запускаем ещё одну итерацию. Если какое-то расстояние всё ещё можно улучшить — мы нашли отрицательный цикл, а значит, арбитражную возможность.
Техника виртуального суперисточника
Наивный подход — запускать Bellman-Ford от каждой вершины отдельно. Это дало бы сложность O(|V|^2 * |E|) — слишком медленно. Вместо этого используется элегантный трюк: инициализируем все расстояния нулём. Это эквивалентно добавлению виртуального суперисточника с рёбрами нулевого веса ко всем вершинам. Теперь за один проход мы обнаруживаем отрицательные циклы, достижимые из любой вершины.
Виртуальный суперисточник S соединён с каждой вершиной ребром нулевого веса. Это позволяет одним проходом Bellman-Ford обнаружить все отрицательные циклы в графе.
Реализация на Rust
use std::collections::HashMap;
#[derive(Clone, Debug)]structArbitrageGraph {
n: usize,
adj: Vec<Vec<(usize, f64)>>,
asset_to_idx: HashMap<String, usize>,
idx_to_asset: Vec<String>,
}
#[derive(Debug)]structArbitrageResult {
cycle: Vec<usize>,
profit_ratio: f64,
}
/// Обнаружение арбитража через Bellman-Fordfndetect_arbitrage_bellman_ford(graph: &ArbitrageGraph) ->Vec<ArbitrageResult> {
letn = graph.n;
letmut dist = vec![0.0_f64; n]; // Виртуальный суперисточникletmut pred = vec![usize::MAX; n];
letmut results = Vec::new();
// Собираем все рёбраletedges: Vec<(usize, usize, f64)> = graph.adj.iter().enumerate()
.flat_map(|(u, neighbors)| {
neighbors.iter().map(move |&(v, w)| (u, v, w))
})
.collect();
// Фаза 1: n-1 итераций релаксацииfor_in0..n - 1 {
letmut updated = false;
for &(u, v, w) in &edges {
if dist[u] + w < dist[v] - 1e-10 {
dist[v] = dist[u] + w;
pred[v] = u;
updated = true;
}
}
if !updated {
break; // Ранняя остановка: нет отрицательных циклов
}
}
// Фаза 2: детектирование отрицательных цикловletmut on_cycle = vec![false; n];
for &(u, v, w) in &edges {
if dist[u] + w < dist[v] - 1e-10 {
// Нашли вершину на отрицательном циклеletmut x = v;
for_in0..n {
x = pred[x];
}
if on_cycle[x] {
continue;
}
// Извлекаем циклletmut cycle = Vec::new();
letstart = x;
letmut current = start;
loop {
cycle.push(current);
on_cycle[current] = true;
current = pred[current];
if current == start {
break;
}
}
cycle.reverse();
// Считаем прибыльностьletprofit_ratio = calculate_profit_ratio(graph, &cycle);
if profit_ratio > 1.0 {
results.push(ArbitrageResult { cycle, profit_ratio });
}
}
}
results.sort_by(|a, b| b.profit_ratio.partial_cmp(&a.profit_ratio).unwrap());
results
}
/// Расчёт коэффициента прибыли для циклаfncalculate_profit_ratio(graph: &ArbitrageGraph, cycle: &[usize]) ->f64 {
letmut total_weight = 0.0;
foriin0..cycle.len() {
letfrom = cycle[i];
letto = cycle[(i + 1) % cycle.len()];
ifletSome(&(_, w)) = graph.adj[from].iter().find(|&&(t, _)| t == to) {
total_weight += w;
}
}
(-total_weight).exp()
}
Обратите внимание на 1e-10 — эпсилон для сравнения чисел с плавающей точкой. Без него ошибки округления будут генерировать ложные срабатывания. Это одна из тех деталей, которые отличают работающий продакшен-код от академического примера.
SPFA: когда Bellman-Ford слишком медленный
SPFA (Shortest Path Faster Algorithm) — оптимизация Bellman-Ford на основе очереди. Вместо того чтобы на каждой итерации перебирать все рёбра, SPFA поддерживает очередь вершин, расстояния до которых были обновлены, и релаксирует только рёбра, исходящие из этих вершин. Наихудшая сложность остаётся O(|V|*|E|), но на практике среднее время работы — O(|E|).
Представьте разницу так: Bellman-Ford — это обход всех комнат в здании на каждом раунде. SPFA — это обход только тех комнат, куда «постучались» на предыдущем шаге. Если обновление затронуло 3 вершины из 500, мы обработаем рёбра только этих 3 вершин, а не все рёбра графа.
Детектирование отрицательных циклов в SPFA
В SPFA мы отслеживаем длину пути до каждой вершины. Если путь до какой-то вершины содержит |V| или более рёбер — мы гарантированно нашли отрицательный цикл (по принципу «голубиных клеток»: в пути из |V| рёбер через |V| вершин хотя бы одна вершина повторяется).
use std::collections::VecDeque;
/// SPFA с ранним обнаружением отрицательных цикловfndetect_arbitrage_spfa(graph: &ArbitrageGraph) ->Option<ArbitrageResult> {
letn = graph.n;
letmut dist = vec![0.0_f64; n];
letmut pred = vec![usize::MAX; n];
letmut path_len = vec![0_u32; n];
letmut in_queue = vec![true; n];
letmut queue: VecDeque<usize> = (0..n).collect();
whileletSome(u) = queue.pop_front() {
in_queue[u] = false;
for &(v, w) in &graph.adj[u] {
if dist[u] + w < dist[v] - 1e-10 {
dist[v] = dist[u] + w;
pred[v] = u;
path_len[v] = path_len[u] + 1;
// Длина пути >= n => отрицательный цикл!if path_len[v] >= n asu32 {
letcycle = trace_cycle(v, &pred, n);
letprofit_ratio = calculate_profit_ratio(graph, &cycle);
returnSome(ArbitrageResult { cycle, profit_ratio });
}
if !in_queue[v] {
// SLF: если dist[v] < dist[front], в начало очередиifletSome(&front) = queue.front() {
if dist[v] < dist[front] {
queue.push_front(v);
} else {
queue.push_back(v);
}
} else {
queue.push_back(v);
}
in_queue[v] = true;
}
}
}
}
None
}
SLF и LLL: тонкая настройка очереди
Две оптимизации, которые значительно ускоряют SPFA на практике:
SLF (Smallest Label First): при добавлении вершины v в очередь, если dist[v] < dist[queue.front()], вставляем v в начало очереди вместо конца. Это приоритизирует более «перспективные» вершины — те, до которых уже найден короткий путь.
LLL (Large Label Last): поддерживаем среднее расстояние вершин в очереди. Если при извлечении вершина имеет расстояние выше среднего, отправляем её обратно в конец. Это предотвращает «застревание» на дальних вершинах.
Есть и более изощрённый подход — проверка графа зависимостей. Каждые n итераций проверяем, есть ли цикл в графе предшественников (pred). Если есть — в исходном графе гарантированно существует отрицательный цикл. Бенчмарки показывают разницу в порядки: 1737 мс для стандартного SPFA против практически мгновенного обнаружения для графов с 10^4 вершинами.
Bellman-Ford перебирает все рёбра на каждой итерации. SPFA обрабатывает только «активные» вершины, что на практике даёт многократное ускорение на разреженных графах.
RICH: революция в обнаружении арбитража (VLDB 2025)
А теперь — самое интересное. RICH (Real-time Identification of negative Cycles for High-efficiency arbitrage) — алгоритм, опубликованный на VLDB 2025, который показывает до 32.69-кратного ускорения по сравнению со стандартным Bellman-Ford для обнаружения арбитража.
RICH решает задачу k-hop Most Negative Cycle (kMNC): поиск отрицательного цикла длиной не более k рёбер с минимальным суммарным весом. Это именно то, что нужно для арбитража: мы хотим найти самый прибыльный цикл, причём ограниченной длины (на практике сделки длиннее 5-6 ног редко выгодны из-за комиссий и проскальзывания).
Техника раскраски (Color-Coding)
Ключевая инновация RICH — адаптация техники раскраски (color-coding), впервые предложенной Alon, Yuster и Zwick в 1995 году, для задачи обнаружения отрицательных циклов.
Идея на удивление красива:
Случайная раскраска. Присваиваем каждой вершине случайный цвет из множества {1, 2, ..., k}.
Поиск «красочных» циклов. Ищем циклы, в которых все вершины имеют различные цвета, используя динамическое программирование.
Вероятностная гарантия. Цикл длины k имеет вероятность k!/k^k быть «красочным». Запуская O(2^k) независимых испытаний, получаем высокую вероятность обнаружения.
Почему это работает? «Красочный» цикл — это цикл, в котором каждый цвет встречается ровно один раз. Если цикл длины k раскрашен k цветами, вероятность того, что все цвета различны, равна k!/k^k (это же задача о дне рождения!). Для k=5 эта вероятность составляет ~3.8%, что кажется мало. Но после 2^5 = 32 независимых попыток вероятность пропустить цикл экспоненциально мала.
Динамическое программирование в RICH
Для каждой вершины v и каждого подмножества цветов S:
dp[v][S] = минимальный вес пути, заканчивающегося в v,
который использует ровно цвета из S (по одной вершине на цвет)
Переход:
dp[v][S] = min по всем рёбрам (u, v):
dp[u][S \ {color(v)}] + w(u, v)
где color(v) ∈ S
Обнаружение цикла:
Для каждой вершины v и полного набора цветов S = {1, ..., k}:
Если dp[v][S] < 0 и существует ребро (v, начало_пути):
Найден отрицательный k-цикл!
Ключевая оптимизация — битовое кодирование множеств цветов. Множество из k цветов представляется k-битной маской, все операции над множествами выполняются за O(1). Для k=6 множество хранится в одном байте, и проверка принадлежности — это одна побитовая операция.
Алгоритм RICH: (a) случайная раскраска вершин, (b) динамическое программирование по подмножествам цветов, (c) обнаружение отрицательного цикла. Битовые маски обеспечивают O(1) операции над множествами.
Почему RICH даёт 32x ускорение
Bellman-Ford ищет любой отрицательный цикл любой длины, делая |V|-1 итераций по всем рёбрам. RICH целенаправленно ищет циклы ограниченной длины k, и его DP-таблица имеет структуру, позволяющую:
Отсечение графа по цветам. Если вершина окрашена в цвет 3, она не может участвовать в путях, множество цветов которых не содержит 3. Это позволяет безопасно удалить большинство вершин и рёбер.
Инкрементальные обновления. При изменении веса ребра (обновление цены) пересчитываются только затронутые состояния DP, а не вся таблица.
Фокус на коротких циклах. На практике арбитражные циклы длиной 3-6 — самые прибыльные. RICH находит их за O(2^k * |V| * |E|), что при k=5 и умеренном размере графа радикально быстрее, чем O(|V| * |E|) для Bellman-Ford, который тратит время на исследование длинных, непрактичных путей.
Сложность RICH:
Время: O(2^k * |V| * |E|) за одну итерацию, O(e^k) итераций для высокой вероятности
Память: O(2^k * |V|)
Ограничение: экспоненциальный фактор 2^k делает алгоритм непрактичным для k > 15
Для типичного арбитражного сканера с k=5 (пятиногие сделки — разумный максимум) это означает 2^5 = 32 состояния на вершину. На графе с 500 вершинами и 2000 рёбрами это ~32 * 500 = 16,000 состояний и ~32 * 2000 = 64,000 переходов за итерацию — молниеносно по сравнению с 499 * 2000 = 998,000 операций для Bellman-Ford.
Line Graph + MMBF: 23,868 путей вместо 19
Если RICH — это снайперская винтовка для коротких циклов, то MMBF (Modified Moore-Bellman-Ford) на линейном графе — это невод, который вылавливает все прибыльные маршруты, включая длинные и нециклические.
Что такое линейный граф
Линейный граф L(G) исходного графа G — это трансформация, в которой:
Каждое ребро (торговая пара / пул ликвидности) в G становится вершиной в L(G)
Две вершины в L(G) соединены, если соответствующие рёбра в G имеют общий токен
Исходный граф G:
Токены: {A, B, C}
Пулы: {A-B, B-C, A-C}
Линейный граф L(G):
Вершины: {AB, BC, AC}
Рёбра: AB--BC (общий токен B)
AB--AC (общий токен A)
BC--AC (общий токен C)
Почему это прорыв
Результаты на данных Uniswap V2 поражают:
Метрика
Стандартный MBF
MMBF
Путей с прибылью > $1,000
19
23,868
Максимальная прибыль на одном пути
~$100K
~$1M
Длина путей
3-4 хопа
3-15 хопов
Время работы (100 токенов, 400 пулов)
~2с
~8-10с
Стандартный MBF нашёл 19 путей. MMBF — 23,868. Разница в три порядка! Почему? Стандартный Bellman-Ford «застревает» на первом найденном отрицательном цикле из 3-4 вершин. MMBF на линейном графе:
Находит циклы от любого указанного токена — можно искать арбитраж, начинающийся именно с USDT или ETH.
Обнаруживает больше циклов за один запуск — минимум один цикл от каждого стартового токена.
Находит нециклические пути — прибыльные маршруты A -> ... -> N, где N != A. Это полезно для ребалансировки портфеля.
Работает с длинными путями — до 15 хопов, где скрывается большая часть неочевидной прибыли.
Стандартный MBF находит единичные короткие циклы. MMBF на линейном графе обнаруживает на порядки больше прибыльных маршрутов, включая длинные цепочки и нециклические пути.
K кратчайших путей: ранжирование возможностей
Найти один арбитражный цикл — полдела. В реальной торговле мы хотим видеть топ-N лучших возможностей, чтобы выбрать оптимальную с учётом ликвидности, скорости исполнения и рисков.
Алгоритм Йена (Yen's algorithm) находит K кратчайших путей без петель от источника к цели. Адаптация для арбитража: для каждого актива A ищем K кратчайших путей от A к A самому себе. Каждый такой путь с отрицательным суммарным весом — арбитражная возможность.
Альтернативный подход — перечисление циклов через приоритетную очередь:
use std::collections::BinaryHeap;
use std::cmp::Reverse;
use ordered_float::OrderedFloat;
/// Поиск top-K арбитражных возможностей по всем активамfnfind_top_k_arbitrage(
graph: &ArbitrageGraph,
k: usize,
max_hops: usize,
) ->Vec<ArbitrageResult> {
letn = graph.n;
letmut heap: BinaryHeap<Reverse<(
OrderedFloat<f64>, usize, Vec<usize>, usize,
)>> = BinaryHeap::new();
letmut results: Vec<ArbitrageResult> = Vec::new();
// Стартуем от каждой вершиныforvin0..n {
heap.push(Reverse((
OrderedFloat(0.0), v, vec![v], v,
)));
}
whileletSome(Reverse((weight, current, path, start))) = heap.pop() {
// Вернулись к старту — цикл найденif path.len() > 1 && current == start {
if weight.0 < -1e-10 {
results.push(ArbitrageResult {
cycle: path,
profit_ratio: (-weight.0).exp(),
});
if results.len() >= k {
break;
}
}
continue;
}
if path.len() > max_hops {
continue;
}
// Расширяем соседейfor &(next, edge_w) in &graph.adj[current] {
if next != start && path.contains(&next) {
continue; // Не повторяем вершины (кроме старта)
}
letmut new_path = path.clone();
new_path.push(next);
heap.push(Reverse((
OrderedFloat(weight.0 + edge_w),
next,
new_path,
start,
)));
}
}
results.sort_by(|a, b| b.profit_ratio.partial_cmp(&a.profit_ratio).unwrap());
results
}
Этот подход прост и гибок: он находит циклы любой длины до max_hops, ранжированные по прибыльности. Однако его сложность растёт экспоненциально с глубиной поиска, поэтому для больших графов предпочтительнее RICH.
Построение мультиинструментального графа
Реальный арбитражный граф — это не просто «валюты и курсы». Он включает разные типы активов, биржи и способы конвертации.
Кросс-биржевая структура
Один и тот же актив на разных биржах — это разные вершины графа, соединённые рёбрами переводов:
Единый граф объединяет спотовые пары, бессрочные и поставочные фьючерсы, межбиржевые переводы. Каждый тип ребра имеет свою модель весов.
Учёт ликвидности
Критический нюанс: обменный курс из стакана имеет ограниченную ликвидность на каждом ценовом уровне. Если арбитражная возможность требует продать 10 BTC, а на лучшем уровне bid стоит только 0.5 BTC, реальный курс будет хуже.
/// Ребро с учётом объёма (несколько ценовых уровней)structVolumeAwareEdge {
tiers: Vec<(f64, f64)>, // (кумулятивный_объём, маржинальный_курс)
}
implVolumeAwareEdge {
/// Эффективный курс для заданного объёма сделкиfneffective_rate(&self, volume: f64) ->f64 {
letmut remaining = volume;
letmut total_output = 0.0;
for &(tier_volume, rate) in &self.tiers {
letfill = remaining.min(tier_volume);
total_output += fill * rate;
remaining -= fill;
if remaining <= 0.0 {
break;
}
}
if remaining > 0.0 {
return0.0; // Недостаточно ликвидности
}
total_output / volume
}
}
Обновления в реальном времени
Крипторынок не ждёт. Цены обновляются сотни раз в секунду через WebSocket-потоки. Граф должен обновляться инкрементально, не пересчитывая всё с нуля.
Инкрементальные обновления рёбер
При изменении цены в стакане обновляются только затронутые рёбра:
Для многопоточной арбитражной системы граф должен поддерживать одновременные чтения (сканирование) и записи (обновление цен). Мы используем три паттерна:
ArcSwap — лучший выбор для арбитража. Lock-free чтение, один писатель создаёт новый снимок графа и атомарно подменяет указатель. Идеально для паттерна «один писатель, много читателей»:
use arc_swap::ArcSwap;
use std::sync::Arc;
structConcurrentArbitrageGraph {
graph: ArcSwap<ArbitrageGraph>,
}
implConcurrentArbitrageGraph {
/// Lock-free чтение для потоков сканированияfnsnapshot(&self) -> arc_swap::Guard<Arc<ArbitrageGraph>> {
self.graph.load()
}
/// Обновление графа (вызывается потоком ценовых данных)fnupdate(&self, new_graph: ArbitrageGraph) {
self.graph.store(Arc::new(new_graph));
}
}
Общая архитектура выглядит так: WebSocket-фид обновляет граф через единственный поток-писатель. Он создаёт новый снимок и атомарно подменяет через ArcSwap. Пул потоков-сканеров берёт последний снимок (lock-free!) и запускает SPFA от разных стартовых вершин. Найденные возможности отправляются через crossbeam-канал в движок исполнения.
Практическое руководство: какой алгоритм когда использовать
После всей теории — конкретные рекомендации. Мы составили матрицу выбора на основе нашего опыта:
Матрица решений
Сценарий
Алгоритм
Почему
< 50 активов, < 200 пар
Bellman-Ford или SPFA
Простота, скорость достаточна
50-500 активов, разреженный граф
SPFA с SLF/LLL
Лучшее среднее время O(E)
50-500 активов, плотный граф
Floyd-Warshall
O(V^3), не зависит от числа рёбер
Нужен топ-K возможностей
K-shortest paths
Ранжированный список
Ограниченная длина цикла (3-6 ног)
RICH
32x ускорение для k-bounded
Нужно найти все пути, включая длинные
MMBF + линейный граф
23,868 vs 19 путей
Стриминг обновлений в реальном времени
Инкрементальный SPFA
Обновляет только затронутые вершины
Мультибиржевой мониторинг
Кросс-биржевой граф + SPFA
Переводы как рёбра с задержкой
Сравнение сложностей
┌──────────────────────┬───────────────┬────────────┬──────────────────────────┐
│ Алгоритм │ Время │ Память │ Лучший случай │
├──────────────────────┼───────────────┼────────────┼──────────────────────────┤
│ Bellman-Ford │ O(VE) │ O(V) │ Простой, надёжный │
│ SPFA │ O(VE) / O(E) │ O(V) │ Быстрый на практике │
│ Floyd-Warshall │ O(V³) │ O(V²) │ Плотные рынки │
│ Johnson's │ O(V²lgV + VE) │ O(V²) │ Все пары, разреженный │
│ RICH (color-coding) │ O(2^k·VE) │ O(2^k·V) │ k-bounded лучший цикл │
│ MMBF (line graph) │ O(M·E_L) │ O(E_L) │ Все пути + нециклические │
│ Yen's K-shortest │ O(KN(M+NlgN)) │ O(KN) │ Топ-K возможностей │
└──────────────────────┴───────────────┴────────────┴──────────────────────────┘
Практические бенчмарки
Система
Латентность
Язык
Примечания
Sub-microsecond HFT
< 50 мкс
C++
Lock-free, SIMD
Профессиональный арб-бот
1-5 мс
C++/Rust
Колоцированные серверы
kucoin_arbitrage
~10-50 мс
Rust
WebSocket, event-driven
Python-фреймворки
100-500 мс
Python
ccxt-based
Честное предупреждение
Несколько важных ограничений, о которых молчат большинство статей:
Фантомный арбитраж. Последовательные API-запросы создают временную рассинхронизацию. Цены меняются между запросами, создавая видимость арбитража из устаревших данных. WebSocket-фиды смягчают, но не устраняют проблему.
Комиссии съедают прибыль. Исследования показывают, что обнаруженные возможности часто дают лишь 0.05-0.087% прибыли — ниже порога комиссий. На DEX добавляется gas fee: за 11 месяцев на Uniswap из 34,429 ETH выручки 8,458 ETH ушло на газ.
Статистика DEX-арбитража: 85% циклических арбитражных транзакций — трёхногие. Это совпадает с нашей рекомендацией использовать RICH с k=3-5.
Риски фьючерсов: базисный риск (спот и фьючерс могут разойтись при экстремальных условиях), ликвидационный риск (левередж), переменность funding rate (ставка меняется каждые 8 часов).
Что дальше
В следующих частях серии мы разберём:
Часть 2: Funding rate арбитраж — как моделировать временные рёбра и оптимизировать доходность cash-and-carry стратегий
Часть 3: GNN-подходы — графовые нейросети для предсказания арбитража до его появления
Часть 4: Полная реализация на Rust — от WebSocket-фида до исполнения ордеров
Графовые алгоритмы — это не просто элегантная математика. Это рабочий инструмент, который превращает хаос крипторынков в структурированное пространство поиска. Bellman-Ford даёт надёжный фундамент. SPFA ускоряет его на практике. RICH выводит на новый уровень для ограниченных циклов. А MMBF раскрывает возможности, которые классические методы просто не видят. Выбирайте инструмент под задачу — и пусть граф работает на вас.
@software{soloviov2026grapharbitragealgorithms,
author = {Soloviov, Eugen},
title = {Графовые алгоритмы для обнаружения арбитража: от Bellman-Ford до RICH},
year = {2026},
url = {https://marketmaker.cc/ru/blog/post/complex-arbitrage-graph-algorithms},
version = {0.1.0},
description = {Как преобразовать криптовалютные рынки в взвешенный ориентированный граф, находить арбитражные возможности через детектирование отрицательных циклов, и почему алгоритм RICH (VLDB 2025) даёт 32-кратное ускорение по сравнению с классическим Bellman-Ford.}
}