[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-14-poisk-arbitrazhnykh-tsiklov-dfs-graf-pulov":3},{"article":4,"author":54},{"id":5,"category_id":6,"title":7,"slug":8,"excerpt":9,"content_md":10,"content_html":11,"locale":12,"author_id":13,"published":14,"published_at":15,"meta_title":7,"meta_description":16,"focus_keyword":17,"og_image":18,"canonical_url":18,"robots_meta":19,"created_at":15,"updated_at":15,"tags":20,"category_name":34,"related_articles":35},"d0000000-0000-0000-0000-000000000214","a0000000-0000-0000-0000-000000000016","Deep EVM #14: Поиск арбитражных циклов — DFS на графе пулов","deep-evm-14-poisk-arbitrazhnykh-tsiklov-dfs-graf-pulov","Алгоритмическая сторона MEV: моделирование DEX-пулов как ориентированного графа и поиск прибыльных циклов с помощью DFS — реализация на Rust.","## Арбитраж как задача на графах\n\nАрбитраж между DEX-пулами — по сути задача поиска прибыльных циклов в ориентированном взвешенном графе. Узлы — токены, рёбра — пулы ликвидности, веса — обменные курсы с учётом комиссий.\n\nЕсли произведение весов рёбер вдоль цикла > 1, цикл прибылен.\n\n## Моделирование графа\n\n### Структуры данных\n\n```rust\nuse std::collections::HashMap;\n\n#[derive(Clone, Debug)]\npub struct Pool {\n    pub address: Address,\n    pub token0: Address,\n    pub token1: Address,\n    pub reserve0: U256,\n    pub reserve1: U256,\n    pub fee: u32,        \u002F\u002F fee в базисных пунктах (30 = 0.3%)\n    pub protocol: Protocol,\n}\n\n#[derive(Clone, Debug)]\npub enum Protocol {\n    UniswapV2,\n    UniswapV3,\n    SushiSwap,\n    Curve,\n}\n\n#[derive(Clone, Debug)]\npub struct Edge {\n    pub pool: Pool,\n    pub token_in: Address,\n    pub token_out: Address,\n}\n\npub struct ArbitrageGraph {\n    \u002F\u002F token_address → vec of edges starting from this token\n    pub adjacency: HashMap\u003CAddress, Vec\u003CEdge>>,\n    pub tokens: Vec\u003CAddress>,\n}\n```\n\n### Построение графа\n\n```rust\nimpl ArbitrageGraph {\n    pub fn new() -> Self {\n        Self {\n            adjacency: HashMap::new(),\n            tokens: Vec::new(),\n        }\n    }\n    \n    pub fn add_pool(&mut self, pool: Pool) {\n        \u002F\u002F Каждый пул создаёт два ребра: token0→token1 и token1→token0\n        let edge_forward = Edge {\n            pool: pool.clone(),\n            token_in: pool.token0,\n            token_out: pool.token1,\n        };\n        let edge_reverse = Edge {\n            pool: pool.clone(),\n            token_in: pool.token1,\n            token_out: pool.token0,\n        };\n        \n        self.adjacency\n            .entry(pool.token0)\n            .or_default()\n            .push(edge_forward);\n        self.adjacency\n            .entry(pool.token1)\n            .or_default()\n            .push(edge_reverse);\n        \n        \u002F\u002F Добавляем токены если новые\n        if !self.adjacency.contains_key(&pool.token0) {\n            self.tokens.push(pool.token0);\n        }\n        if !self.adjacency.contains_key(&pool.token1) {\n            self.tokens.push(pool.token1);\n        }\n    }\n}\n```\n\n## Расчёт обменного курса\n\nДля AMM с формулой `x * y = k` (Uniswap V2):\n\n```rust\npub fn get_amount_out(\n    amount_in: U256,\n    reserve_in: U256,\n    reserve_out: U256,\n    fee_bps: u32,\n) -> U256 {\n    \u002F\u002F amount_out = reserve_out * amount_in_with_fee\n    \u002F\u002F              \u002F (reserve_in * 10000 + amount_in_with_fee)\n    let fee_factor = 10000 - fee_bps;  \u002F\u002F 9970 для 0.3% комиссии\n    let amount_in_with_fee = amount_in * U256::from(fee_factor);\n    let numerator = reserve_out * amount_in_with_fee;\n    let denominator = reserve_in * U256::from(10000) + amount_in_with_fee;\n    numerator \u002F denominator\n}\n```\n\nДля ребра графа:\n\n```rust\nimpl Edge {\n    pub fn get_output(&self, amount_in: U256) -> U256 {\n        let (reserve_in, reserve_out) = if self.token_in == self.pool.token0 {\n            (self.pool.reserve0, self.pool.reserve1)\n        } else {\n            (self.pool.reserve1, self.pool.reserve0)\n        };\n        get_amount_out(amount_in, reserve_in, reserve_out, self.pool.fee)\n    }\n}\n```\n\n## Поиск циклов: DFS\n\nНаивный подход — перечислить все циклы длины 2-4, начинающиеся и заканчивающиеся на WETH (или другом базовом токене):\n\n```rust\n#[derive(Clone, Debug)]\npub struct ArbitrageRoute {\n    pub edges: Vec\u003CEdge>,\n    pub profit_ratio: f64,  \u002F\u002F > 1.0 означает прибыль\n}\n\nimpl ArbitrageGraph {\n    pub fn find_cycles(\n        &self,\n        start_token: Address,\n        max_depth: usize,\n    ) -> Vec\u003CArbitrageRoute> {\n        let mut routes = Vec::new();\n        let mut path = Vec::new();\n        let mut visited_pools = std::collections::HashSet::new();\n        \n        self.dfs(\n            start_token,\n            start_token,\n            &mut path,\n            &mut visited_pools,\n            max_depth,\n            &mut routes,\n        );\n        \n        routes\n    }\n    \n    fn dfs(\n        &self,\n        current: Address,\n        target: Address,\n        path: &mut Vec\u003CEdge>,\n        visited_pools: &mut std::collections::HashSet\u003CAddress>,\n        max_depth: usize,\n        routes: &mut Vec\u003CArbitrageRoute>,\n    ) {\n        if path.len() > max_depth {\n            return;\n        }\n        \n        \u002F\u002F Если мы вернулись к стартовому токену (и прошли хотя бы 2 ребра)\n        if path.len() >= 2 && current == target {\n            let ratio = self.calculate_profit_ratio(path);\n            if ratio > 1.0 {\n                routes.push(ArbitrageRoute {\n                    edges: path.clone(),\n                    profit_ratio: ratio,\n                });\n            }\n            return;\n        }\n        \n        if let Some(edges) = self.adjacency.get(&current) {\n            for edge in edges {\n                \u002F\u002F Не используем один пул дважды в одном пути\n                if visited_pools.contains(&edge.pool.address) {\n                    continue;\n                }\n                \n                visited_pools.insert(edge.pool.address);\n                path.push(edge.clone());\n                \n                self.dfs(\n                    edge.token_out,\n                    target,\n                    path,\n                    visited_pools,\n                    max_depth,\n                    routes,\n                );\n                \n                path.pop();\n                visited_pools.remove(&edge.pool.address);\n            }\n        }\n    }\n    \n    fn calculate_profit_ratio(&self, path: &[Edge]) -> f64 {\n        \u002F\u002F Используем логарифмы для избежания overflow\n        let mut log_ratio = 0.0_f64;\n        for edge in path {\n            let (reserve_in, reserve_out) = if edge.token_in == edge.pool.token0 {\n                (edge.pool.reserve0, edge.pool.reserve1)\n            } else {\n                (edge.pool.reserve1, edge.pool.reserve0)\n            };\n            let fee_factor = (10000 - edge.pool.fee) as f64 \u002F 10000.0;\n            let rate = reserve_out.as_u128() as f64\n                \u002F reserve_in.as_u128() as f64\n                * fee_factor;\n            log_ratio += rate.ln();\n        }\n        log_ratio.exp()\n    }\n}\n```\n\n## Оптимизация DFS\n\n### Pruning — отсечение бесперспективных путей\n\nНа каждом шаге DFS вычисляем текущий обменный курс. Если он уже ниже порога прибыльности — отсекаем ветку:\n\n```rust\nfn dfs_with_pruning(\n    &self,\n    current: Address,\n    target: Address,\n    path: &mut Vec\u003CEdge>,\n    visited_pools: &mut HashSet\u003CAddress>,\n    max_depth: usize,\n    current_amount: U256,  \u002F\u002F текущий баланс по пути\n    start_amount: U256,    \u002F\u002F стартовый баланс\n    routes: &mut Vec\u003CArbitrageRoute>,\n) {\n    if path.len() > max_depth {\n        return;\n    }\n    \n    if path.len() >= 2 && current == target {\n        if current_amount > start_amount {\n            let profit = current_amount - start_amount;\n            routes.push(ArbitrageRoute {\n                edges: path.clone(),\n                profit_ratio: current_amount.as_u128() as f64\n                    \u002F start_amount.as_u128() as f64,\n            });\n        }\n        return;\n    }\n    \n    \u002F\u002F Pruning: если текущий баланс \u003C 50% от стартового — бесперспективно\n    if current_amount \u003C start_amount \u002F 2 {\n        return;\n    }\n    \n    \u002F\u002F ... продолжение DFS\n}\n```\n\n### Параллелизация\n\nКаждый стартовый токен можно обрабатывать в отдельном потоке:\n\n```rust\nuse rayon::prelude::*;\n\npub fn find_all_arbitrage(graph: &ArbitrageGraph) -> Vec\u003CArbitrageRoute> {\n    let base_tokens = vec![WETH, USDC, USDT, DAI, WBTC];\n    \n    base_tokens\n        .par_iter()\n        .flat_map(|token| {\n            graph.find_cycles(*token, 4)\n        })\n        .collect()\n}\n```\n\n## Алгоритм Беллмана-Форда для обнаружения отрицательных циклов\n\nАльтернатива DFS — классический алгоритм Беллмана-Форда. Если веса рёбер — отрицательные логарифмы обменных курсов, то отрицательный цикл = прибыльный арбитраж:\n\n```rust\npub fn bellman_ford_negative_cycles(\n    graph: &ArbitrageGraph,\n    source: Address,\n) -> Vec\u003CVec\u003CAddress>> {\n    let n = graph.tokens.len();\n    let mut dist: HashMap\u003CAddress, f64> = HashMap::new();\n    let mut pred: HashMap\u003CAddress, Option\u003C(Address, Edge)>> = HashMap::new();\n    \n    for token in &graph.tokens {\n        dist.insert(*token, f64::INFINITY);\n        pred.insert(*token, None);\n    }\n    dist.insert(source, 0.0);\n    \n    \u002F\u002F Релаксация N-1 раз\n    for _ in 0..n - 1 {\n        for (token, edges) in &graph.adjacency {\n            for edge in edges {\n                let weight = -compute_log_rate(edge);  \u002F\u002F отрицательный логарифм\n                let new_dist = dist[token] + weight;\n                if new_dist \u003C dist[&edge.token_out] {\n                    dist.insert(edge.token_out, new_dist);\n                    pred.insert(edge.token_out, Some((*token, edge.clone())));\n                }\n            }\n        }\n    }\n    \n    \u002F\u002F N-я итерация: если ещё возможна релаксация — найден отрицательный цикл\n    let mut cycles = Vec::new();\n    for (token, edges) in &graph.adjacency {\n        for edge in edges {\n            let weight = -compute_log_rate(edge);\n            if dist[token] + weight \u003C dist[&edge.token_out] {\n                \u002F\u002F Обнаружен отрицательный цикл — восстанавливаем путь\n                let cycle = reconstruct_cycle(&pred, edge.token_out);\n                cycles.push(cycle);\n            }\n        }\n    }\n    \n    cycles\n}\n\nfn compute_log_rate(edge: &Edge) -> f64 {\n    let (reserve_in, reserve_out) = if edge.token_in == edge.pool.token0 {\n        (edge.pool.reserve0, edge.pool.reserve1)\n    } else {\n        (edge.pool.reserve1, edge.pool.reserve0)\n    };\n    let fee_factor = (10000 - edge.pool.fee) as f64 \u002F 10000.0;\n    let rate = reserve_out.as_u128() as f64 \u002F reserve_in.as_u128() as f64 * fee_factor;\n    rate.ln()\n}\n```\n\n## Обновление графа в реальном времени\n\nГраф должен обновляться при каждом новом блоке и при получении pending-транзакций:\n\n```rust\npub async fn update_reserves(\n    graph: &mut ArbitrageGraph,\n    provider: &Provider\u003CWs>,\n    pools: &[Address],\n) {\n    for pool_addr in pools {\n        let reserves = get_reserves(provider, *pool_addr).await;\n        if let Some(edges) = graph.adjacency.values_mut().flatten().find(|e| {\n            e.pool.address == *pool_addr\n        }) {\n            edges.pool.reserve0 = reserves.0;\n            edges.pool.reserve1 = reserves.1;\n        }\n    }\n}\n```\n\n## Производительность\n\nДля основной сети Ethereum:\n\n| Параметр | Значение |\n|----------|----------|\n| Кол-во пулов (Uniswap V2 + Sushi) | ~50,000 |\n| Кол-во уникальных токенов | ~30,000 |\n| Циклы длины 2 (парные) | ~5,000 |\n| Циклы длины 3 (треугольные) | ~500,000 |\n| Время DFS (max_depth=3) | ~50 мс |\n| Время DFS (max_depth=4) | ~2 сек |\n\nДля MEV критично уложиться в 12 секунд (время слота). На практике серчеры ограничиваются глубиной 3-4 и фильтруют пулы по минимальной ликвидности.\n\n## Итоги\n\nПоиск арбитражных возможностей — задача на графах. DFS с pruning — простой и эффективный подход для циклов длины 2-4. Беллман-Форд обнаруживает отрицательные циклы через логарифмические веса. Параллелизация через rayon ускоряет перебор. В следующей статье мы рассмотрим симуляцию найденных маршрутов — бинарный поиск оптимального размера и форк стейта.","\u003Ch2 id=\"\">Арбитраж как задача на графах\u003C\u002Fh2>\n\u003Cp>Арбитраж между DEX-пулами — по сути задача поиска прибыльных циклов в ориентированном взвешенном графе. Узлы — токены, рёбра — пулы ликвидности, веса — обменные курсы с учётом комиссий.\u003C\u002Fp>\n\u003Cp>Если произведение весов рёбер вдоль цикла &gt; 1, цикл прибылен.\u003C\u002Fp>\n\u003Ch2 id=\"\">Моделирование графа\u003C\u002Fh2>\n\u003Ch3>Структуры данных\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-rust\">use std::collections::HashMap;\n\n#[derive(Clone, Debug)]\npub struct Pool {\n    pub address: Address,\n    pub token0: Address,\n    pub token1: Address,\n    pub reserve0: U256,\n    pub reserve1: U256,\n    pub fee: u32,        \u002F\u002F fee в базисных пунктах (30 = 0.3%)\n    pub protocol: Protocol,\n}\n\n#[derive(Clone, Debug)]\npub enum Protocol {\n    UniswapV2,\n    UniswapV3,\n    SushiSwap,\n    Curve,\n}\n\n#[derive(Clone, Debug)]\npub struct Edge {\n    pub pool: Pool,\n    pub token_in: Address,\n    pub token_out: Address,\n}\n\npub struct ArbitrageGraph {\n    \u002F\u002F token_address → vec of edges starting from this token\n    pub adjacency: HashMap&lt;Address, Vec&lt;Edge&gt;&gt;,\n    pub tokens: Vec&lt;Address&gt;,\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Построение графа\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-rust\">impl ArbitrageGraph {\n    pub fn new() -&gt; Self {\n        Self {\n            adjacency: HashMap::new(),\n            tokens: Vec::new(),\n        }\n    }\n    \n    pub fn add_pool(&amp;mut self, pool: Pool) {\n        \u002F\u002F Каждый пул создаёт два ребра: token0→token1 и token1→token0\n        let edge_forward = Edge {\n            pool: pool.clone(),\n            token_in: pool.token0,\n            token_out: pool.token1,\n        };\n        let edge_reverse = Edge {\n            pool: pool.clone(),\n            token_in: pool.token1,\n            token_out: pool.token0,\n        };\n        \n        self.adjacency\n            .entry(pool.token0)\n            .or_default()\n            .push(edge_forward);\n        self.adjacency\n            .entry(pool.token1)\n            .or_default()\n            .push(edge_reverse);\n        \n        \u002F\u002F Добавляем токены если новые\n        if !self.adjacency.contains_key(&amp;pool.token0) {\n            self.tokens.push(pool.token0);\n        }\n        if !self.adjacency.contains_key(&amp;pool.token1) {\n            self.tokens.push(pool.token1);\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">Расчёт обменного курса\u003C\u002Fh2>\n\u003Cp>Для AMM с формулой \u003Ccode>x * y = k\u003C\u002Fcode> (Uniswap V2):\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">pub fn get_amount_out(\n    amount_in: U256,\n    reserve_in: U256,\n    reserve_out: U256,\n    fee_bps: u32,\n) -&gt; U256 {\n    \u002F\u002F amount_out = reserve_out * amount_in_with_fee\n    \u002F\u002F              \u002F (reserve_in * 10000 + amount_in_with_fee)\n    let fee_factor = 10000 - fee_bps;  \u002F\u002F 9970 для 0.3% комиссии\n    let amount_in_with_fee = amount_in * U256::from(fee_factor);\n    let numerator = reserve_out * amount_in_with_fee;\n    let denominator = reserve_in * U256::from(10000) + amount_in_with_fee;\n    numerator \u002F denominator\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Для ребра графа:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">impl Edge {\n    pub fn get_output(&amp;self, amount_in: U256) -&gt; U256 {\n        let (reserve_in, reserve_out) = if self.token_in == self.pool.token0 {\n            (self.pool.reserve0, self.pool.reserve1)\n        } else {\n            (self.pool.reserve1, self.pool.reserve0)\n        };\n        get_amount_out(amount_in, reserve_in, reserve_out, self.pool.fee)\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"dfs\">Поиск циклов: DFS\u003C\u002Fh2>\n\u003Cp>Наивный подход — перечислить все циклы длины 2-4, начинающиеся и заканчивающиеся на WETH (или другом базовом токене):\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">#[derive(Clone, Debug)]\npub struct ArbitrageRoute {\n    pub edges: Vec&lt;Edge&gt;,\n    pub profit_ratio: f64,  \u002F\u002F &gt; 1.0 означает прибыль\n}\n\nimpl ArbitrageGraph {\n    pub fn find_cycles(\n        &amp;self,\n        start_token: Address,\n        max_depth: usize,\n    ) -&gt; Vec&lt;ArbitrageRoute&gt; {\n        let mut routes = Vec::new();\n        let mut path = Vec::new();\n        let mut visited_pools = std::collections::HashSet::new();\n        \n        self.dfs(\n            start_token,\n            start_token,\n            &amp;mut path,\n            &amp;mut visited_pools,\n            max_depth,\n            &amp;mut routes,\n        );\n        \n        routes\n    }\n    \n    fn dfs(\n        &amp;self,\n        current: Address,\n        target: Address,\n        path: &amp;mut Vec&lt;Edge&gt;,\n        visited_pools: &amp;mut std::collections::HashSet&lt;Address&gt;,\n        max_depth: usize,\n        routes: &amp;mut Vec&lt;ArbitrageRoute&gt;,\n    ) {\n        if path.len() &gt; max_depth {\n            return;\n        }\n        \n        \u002F\u002F Если мы вернулись к стартовому токену (и прошли хотя бы 2 ребра)\n        if path.len() &gt;= 2 &amp;&amp; current == target {\n            let ratio = self.calculate_profit_ratio(path);\n            if ratio &gt; 1.0 {\n                routes.push(ArbitrageRoute {\n                    edges: path.clone(),\n                    profit_ratio: ratio,\n                });\n            }\n            return;\n        }\n        \n        if let Some(edges) = self.adjacency.get(&amp;current) {\n            for edge in edges {\n                \u002F\u002F Не используем один пул дважды в одном пути\n                if visited_pools.contains(&amp;edge.pool.address) {\n                    continue;\n                }\n                \n                visited_pools.insert(edge.pool.address);\n                path.push(edge.clone());\n                \n                self.dfs(\n                    edge.token_out,\n                    target,\n                    path,\n                    visited_pools,\n                    max_depth,\n                    routes,\n                );\n                \n                path.pop();\n                visited_pools.remove(&amp;edge.pool.address);\n            }\n        }\n    }\n    \n    fn calculate_profit_ratio(&amp;self, path: &amp;[Edge]) -&gt; f64 {\n        \u002F\u002F Используем логарифмы для избежания overflow\n        let mut log_ratio = 0.0_f64;\n        for edge in path {\n            let (reserve_in, reserve_out) = if edge.token_in == edge.pool.token0 {\n                (edge.pool.reserve0, edge.pool.reserve1)\n            } else {\n                (edge.pool.reserve1, edge.pool.reserve0)\n            };\n            let fee_factor = (10000 - edge.pool.fee) as f64 \u002F 10000.0;\n            let rate = reserve_out.as_u128() as f64\n                \u002F reserve_in.as_u128() as f64\n                * fee_factor;\n            log_ratio += rate.ln();\n        }\n        log_ratio.exp()\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"dfs\">Оптимизация DFS\u003C\u002Fh2>\n\u003Ch3>Pruning — отсечение бесперспективных путей\u003C\u002Fh3>\n\u003Cp>На каждом шаге DFS вычисляем текущий обменный курс. Если он уже ниже порога прибыльности — отсекаем ветку:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">fn dfs_with_pruning(\n    &amp;self,\n    current: Address,\n    target: Address,\n    path: &amp;mut Vec&lt;Edge&gt;,\n    visited_pools: &amp;mut HashSet&lt;Address&gt;,\n    max_depth: usize,\n    current_amount: U256,  \u002F\u002F текущий баланс по пути\n    start_amount: U256,    \u002F\u002F стартовый баланс\n    routes: &amp;mut Vec&lt;ArbitrageRoute&gt;,\n) {\n    if path.len() &gt; max_depth {\n        return;\n    }\n    \n    if path.len() &gt;= 2 &amp;&amp; current == target {\n        if current_amount &gt; start_amount {\n            let profit = current_amount - start_amount;\n            routes.push(ArbitrageRoute {\n                edges: path.clone(),\n                profit_ratio: current_amount.as_u128() as f64\n                    \u002F start_amount.as_u128() as f64,\n            });\n        }\n        return;\n    }\n    \n    \u002F\u002F Pruning: если текущий баланс &lt; 50% от стартового — бесперспективно\n    if current_amount &lt; start_amount \u002F 2 {\n        return;\n    }\n    \n    \u002F\u002F ... продолжение DFS\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Параллелизация\u003C\u002Fh3>\n\u003Cp>Каждый стартовый токен можно обрабатывать в отдельном потоке:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use rayon::prelude::*;\n\npub fn find_all_arbitrage(graph: &amp;ArbitrageGraph) -&gt; Vec&lt;ArbitrageRoute&gt; {\n    let base_tokens = vec![WETH, USDC, USDT, DAI, WBTC];\n    \n    base_tokens\n        .par_iter()\n        .flat_map(|token| {\n            graph.find_cycles(*token, 4)\n        })\n        .collect()\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">Алгоритм Беллмана-Форда для обнаружения отрицательных циклов\u003C\u002Fh2>\n\u003Cp>Альтернатива DFS — классический алгоритм Беллмана-Форда. Если веса рёбер — отрицательные логарифмы обменных курсов, то отрицательный цикл = прибыльный арбитраж:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">pub fn bellman_ford_negative_cycles(\n    graph: &amp;ArbitrageGraph,\n    source: Address,\n) -&gt; Vec&lt;Vec&lt;Address&gt;&gt; {\n    let n = graph.tokens.len();\n    let mut dist: HashMap&lt;Address, f64&gt; = HashMap::new();\n    let mut pred: HashMap&lt;Address, Option&lt;(Address, Edge)&gt;&gt; = HashMap::new();\n    \n    for token in &amp;graph.tokens {\n        dist.insert(*token, f64::INFINITY);\n        pred.insert(*token, None);\n    }\n    dist.insert(source, 0.0);\n    \n    \u002F\u002F Релаксация N-1 раз\n    for _ in 0..n - 1 {\n        for (token, edges) in &amp;graph.adjacency {\n            for edge in edges {\n                let weight = -compute_log_rate(edge);  \u002F\u002F отрицательный логарифм\n                let new_dist = dist[token] + weight;\n                if new_dist &lt; dist[&amp;edge.token_out] {\n                    dist.insert(edge.token_out, new_dist);\n                    pred.insert(edge.token_out, Some((*token, edge.clone())));\n                }\n            }\n        }\n    }\n    \n    \u002F\u002F N-я итерация: если ещё возможна релаксация — найден отрицательный цикл\n    let mut cycles = Vec::new();\n    for (token, edges) in &amp;graph.adjacency {\n        for edge in edges {\n            let weight = -compute_log_rate(edge);\n            if dist[token] + weight &lt; dist[&amp;edge.token_out] {\n                \u002F\u002F Обнаружен отрицательный цикл — восстанавливаем путь\n                let cycle = reconstruct_cycle(&amp;pred, edge.token_out);\n                cycles.push(cycle);\n            }\n        }\n    }\n    \n    cycles\n}\n\nfn compute_log_rate(edge: &amp;Edge) -&gt; f64 {\n    let (reserve_in, reserve_out) = if edge.token_in == edge.pool.token0 {\n        (edge.pool.reserve0, edge.pool.reserve1)\n    } else {\n        (edge.pool.reserve1, edge.pool.reserve0)\n    };\n    let fee_factor = (10000 - edge.pool.fee) as f64 \u002F 10000.0;\n    let rate = reserve_out.as_u128() as f64 \u002F reserve_in.as_u128() as f64 * fee_factor;\n    rate.ln()\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">Обновление графа в реальном времени\u003C\u002Fh2>\n\u003Cp>Граф должен обновляться при каждом новом блоке и при получении pending-транзакций:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">pub async fn update_reserves(\n    graph: &amp;mut ArbitrageGraph,\n    provider: &amp;Provider&lt;Ws&gt;,\n    pools: &amp;[Address],\n) {\n    for pool_addr in pools {\n        let reserves = get_reserves(provider, *pool_addr).await;\n        if let Some(edges) = graph.adjacency.values_mut().flatten().find(|e| {\n            e.pool.address == *pool_addr\n        }) {\n            edges.pool.reserve0 = reserves.0;\n            edges.pool.reserve1 = reserves.1;\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">Производительность\u003C\u002Fh2>\n\u003Cp>Для основной сети Ethereum:\u003C\u002Fp>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Параметр\u003C\u002Fth>\u003Cth>Значение\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>Кол-во пулов (Uniswap V2 + Sushi)\u003C\u002Ftd>\u003Ctd>~50,000\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Кол-во уникальных токенов\u003C\u002Ftd>\u003Ctd>~30,000\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Циклы длины 2 (парные)\u003C\u002Ftd>\u003Ctd>~5,000\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Циклы длины 3 (треугольные)\u003C\u002Ftd>\u003Ctd>~500,000\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Время DFS (max_depth=3)\u003C\u002Ftd>\u003Ctd>~50 мс\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Время DFS (max_depth=4)\u003C\u002Ftd>\u003Ctd>~2 сек\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>Для MEV критично уложиться в 12 секунд (время слота). На практике серчеры ограничиваются глубиной 3-4 и фильтруют пулы по минимальной ликвидности.\u003C\u002Fp>\n\u003Ch2 id=\"\">Итоги\u003C\u002Fh2>\n\u003Cp>Поиск арбитражных возможностей — задача на графах. DFS с pruning — простой и эффективный подход для циклов длины 2-4. Беллман-Форд обнаруживает отрицательные циклы через логарифмические веса. Параллелизация через rayon ускоряет перебор. В следующей статье мы рассмотрим симуляцию найденных маршрутов — бинарный поиск оптимального размера и форк стейта.\u003C\u002Fp>\n","ru","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:23.669950Z","Алгоритмы поиска MEV-арбитража: моделирование DEX-пулов как графа, DFS с pruning, Беллман-Форд и параллелизация на Rust.","mev арбитраж dfs граф пулов",null,"index, follow",[21,26,30],{"id":22,"name":23,"slug":24,"created_at":25},"c0000000-0000-0000-0000-000000000016","EVM","evm","2026-03-28T10:44:21.513630Z",{"id":27,"name":28,"slug":29,"created_at":25},"c0000000-0000-0000-0000-000000000019","MEV","mev",{"id":31,"name":32,"slug":33,"created_at":25},"c0000000-0000-0000-0000-000000000001","Rust","rust","Инженерия",[36,42,48],{"id":37,"title":38,"slug":39,"excerpt":40,"locale":12,"category_name":34,"published_at":41},"d0200000-0000-0000-0000-000000000013","Почему Бали становится хабом импакт-технологий Юго-Восточной Азии в 2026 году","pochemu-bali-stanovitsya-khabom-impakt-tekhnologiy-2026","Бали занимает 16-е место среди стартап-экосистем Юго-Восточной Азии. Растущая концентрация Web3-разработчиков, ИИ-стартапов в области устойчивого развития и компаний в сфере эко-тревел-технологий формирует нишу столицы импакт-технологий региона.","2026-03-28T10:44:37.953039Z",{"id":43,"title":44,"slug":45,"excerpt":46,"locale":12,"category_name":34,"published_at":47},"d0200000-0000-0000-0000-000000000012","Защита данных в ASEAN: чек-лист разработчика для мультистранового комплаенса","zashchita-dannykh-asean-chek-list-razrabotchika-komplaens","Семь стран ASEAN имеют собственные законы о защите данных с разными моделями согласия, требованиями к локализации и штрафами. Практический чек-лист для разработчиков мультистрановых приложений.","2026-03-28T10:44:37.944001Z",{"id":49,"title":50,"slug":51,"excerpt":52,"locale":12,"category_name":34,"published_at":53},"d0200000-0000-0000-0000-000000000011","Цифровая трансформация Индонезии на $29 миллиардов: возможности для софтверных компаний","tsifrovaya-transformatsiya-indonezii-29-milliardov-vozmozhnosti-dlya-kompaniy","Рынок IT-услуг Индонезии вырастет с $24,37 млрд в 2025 году до $29,03 млрд в 2026 году. Облачная инфраструктура, искусственный интеллект, электронная коммерция и дата-центры обеспечивают самый быстрый рост в Юго-Восточной Азии.","2026-03-28T10:44:37.917095Z",{"id":13,"name":55,"slug":56,"bio":57,"photo_url":18,"linkedin":18,"role":58,"created_at":59,"updated_at":59},"Open Soft Team","open-soft-team","The engineering team at Open Soft, building premium software solutions from Bali, Indonesia.","Engineering Team","2026-03-28T08:31:22.226811Z"]