Добро пожаловать во вторую часть нашей серии статей об открытии прибыльных арбитражных возможностей! В прошлой статье мы подробно рассмотрели определение возможностей треугольного арбитража с помощью The Graph, однако это была только половина дела. Чтобы добиться успеха, мы также должны оценивать каждую потенциальную сделку, чтобы определить прибыльность. Как сказал Тони Сопрано: «Дело не в том, сколько вы зарабатываете, а в том, сколько вы сохраняете». И это именно то, что мы рассмотрим в этой статье — как проверить потенциальные возможности арбитража и обеспечить максимальную прибыльность.

Что мы рассмотрим

  • Запросы GraphQL к The Graph
  • Математика Uniswap V3 CFMM
  • Взаимодействие смарт-контракта с ABI (бинарный интерфейс приложения)

Начнем с того, на чем остановились

В нашем предыдущем посте мы использовали The Graph для индексации данных пула ликвидности из основной сети Ethereum и создали алгоритм для определения торговых путей. Теперь наш следующий шаг — определить прибыльность каждого пути. Но как мы можем это сделать?

Мы разбиваем наш процесс на несколько шагов

  • Рассчитать процентную разницу в цене
  • Поиск оптимальных пулов ликвидности для заимствования активов (Loan Pools)
  • Определить оптимальную сумму ввода токена
  • Подтвердите прибыльность с помощью проверки в сети

Я . Разница в цене в процентах

В треугольном арбитраже мы стремимся извлечь выгоду из расхождений в курсах токенов между тремя пулами ликвидности для получения прибыли. Чтобы определить такие возможности, мы используем ценовые процентные различия (PPD) в качестве ключевого показателя, который включает в себя определение различий в обмене между тремя LP.

Сосредоточение внимания на путях, предлагающих благоприятную PPD, дает два ключевых преимущества. Во-первых, он упрощает наш процесс поиска, сужая потенциально выгодные пути, тем самым сокращая вычислительные затраты. Во-вторых, это позволяет нам определить наиболее прибыльный путь, чтобы максимизировать нашу потенциальную прибыль.

А. Разница в цене в процентах: отправная точка

check_all_structured_paths() — перебирает наш массив сопоставленных объектов и передает каждый путь в find_most_profitable_permutation(). Эта функция вычисляет наиболее выгодную процентную разницу в цене для заданного пути. Если PPD превышает указанный нами порог, мы добавляем путь в наш список потенциальных кандидатов.

function check_all_structured_paths(paths) {
  const inital_check_profitable_paths = [];

  for (const { path, token_ids, pool_addresses } of paths) {
    const most_profitable_permutation = find_most_profitable_permutation(path);

    if (
      most_profitable_permutation.price_percentage_difference >
      PRICE_PERCENTAGE_DIFFERENCE_THRESHOLD
    ) {
      most_profitable_permutation.token_ids = token_ids;

      most_profitable_permutation.pool_addresses = pool_addresses;

      inital_check_profitable_paths.push(most_profitable_permutation);
    }
  }
  return inital_check_profitable_paths;
}

Б. Разница в процентах в цене: перестановки и расчеты

function find_most_profitable_permutation(path) {
  function find_permutations(path_array, temp) {
    let current;
    if (!path_array.length) {
      const calculated_path_and_difference =
        calculate_percentage_difference_of_path(temp);

      all_permutations_for_order =
        all_permutations_for_order.price_percentage_difference <
        calculated_path_and_difference.price_percentage_difference
          ? calculated_path_and_difference
          : all_permutations_for_order;
    }
    for (let i = 0; i < path_array.length; i++) {
      current = path_array.splice(i, 1)[0];
      find_permutations(path_array, temp.concat(current));
      path_array.splice(i, 0, current);
    }
  }

  let all_permutations_for_order = {
    price_percentage_difference: 0,
  };
  find_permutations(path, []);

  return all_permutations_for_order;
}

find_most_profitable_permutation() — генерирует все возможные перестановки нашего пути. Поскольку математически возможных вариантов шесть, мы рассчитываем каждый и выявляем наиболее прибыльный. Для этого мы начинаем с базовой точки 0 и взвешиваем результаты каждой перестановки на каждой итерации. После завершения процесса перестановки функция возвращает объект, включая рассчитанную процентную разницу в цене для наиболее оптимальной перестановки.

calculate_percentage_difference_of_path() — проходит наш путь и рассчитывает каждую сделку, используя данные, предоставленные текущим пулом ликвидности. Для этого мы начинаем с базовой входной суммы ΔY = 1 и обновляем ее на основе результатов примененного метода расчета.

function calculate_percentage_difference_of_path(path) {

  verfiy_token_path(path);

  let arbitrary_amount = 1;

  for (const liqudity_pool of path) {

    if (liqudity_pool.exchange === 'uniswapV3') {
      //calculate token amount out with uniswap v3
      arbitrary_amount = uniswap_V3_swap_math(liqudity_pool, arbitrary_amount);
    } else {
      //calculate token amount out with uniswap v2 and sushiswap
      arbitrary_amount = uniswap_V2_sushiswap_swap_math(
        liqudity_pool,
        arbitrary_amount
      );
    }
  }

  const starting_price = 1;

  const current_price = arbitrary_amount;

  const absoluteDifference = current_price - starting_price;

  const average = (current_price + starting_price) / 2;

  const price_percentage_difference = (absoluteDifference / average) * 100;

  return {
    price_percentage_difference: price_percentage_difference,
    path: path,
  };
}

Методы расчета на каждой итерации определяются биржей, на которой размещается пул ликвидности. Вспомните нашу предыдущую статью, иллюстрирующую различия между нашими DEX и то, как каждая формула будет определять сумму, полученную в результате сделки.

Первый способ: Uniswap V2 и Sushiswap

uniswap_V2_sushiswap_swap_math() — вычисляет окончательную сумму, полученную в результате сделки, используя данные о резервах токенов пула ликвидности.

function uniswap_V2_sushiswap_swap_math(pool, amount) {
   const token_in_reserves =
     pool.token_in === pool.token0.id
       ? Number(pool.reserve0)
       : Number(pool.reserve1);

   const token_out_reserves =
     pool.token_out === pool.token0.id
       ? Number(pool.reserve0)
       : Number(pool.reserve1);

    const calculated_amount = Math.abs(
      (token_in_reserves * token_out_reserves) / (token_in_reserves + amount) -
        token_out_reserves
    );

    return calculated_amount;
}

Мы определяем резервы входных и выходных токенов с помощью UUID входных и выходных токенов, которые устанавливаются с помощью verify_token_path(). Затем мы учитываем текущее состояние нашей суммы сделки и вводим каждую из этих переменных для расчета обновленной суммы сделки.

Метод второй: Uniswap V3

uniswap_V3_swap_math() — определяет окончательную сумму, полученную в результате сделки, используя как текущую ликвидность, так и квадратный корень цены из пула ликвидности.

function uniswap_V3_swap_math(pool, amount) {
  const token_0 = pool.token_in === pool.token0.id;
  const q96 = 2 ** 96;
  const token_0_decimals = 10 ** Number(pool.token0.decimals);
  const token_1_decimals = 10 ** Number(pool.token1.decimals);
  const liquidty = Number(pool.liquidity);
  const current_sqrt_price = Number(pool.sqrtPrice);

  function calc_amount0(liq, pa, pb) {
    if (pa > pb) {
      [pa, pb] = [pb, pa];
    }
    return Number((liq * q96 * (pb - pa)) / pb / pa);
  }

  function calc_amount1(liq, pa, pb) {
    if (pa > pb) {
      [pa, pb] = [pb, pa];
    }
    return Number((liq * (pb - pa)) / q96);
  }

  if (token_0) {

    const amount_in = amount * token_0_decimals;

    const price_next =
      (liquidty * q96 * current_sqrt_price) /
      (liquidty * q96 + amount_in * current_sqrt_price);

    const output = calc_amount1(
      liquidty,
      price_next,
      Number(current_sqrt_price)
    );

    return output / token_1_decimals;

  } else {

    const amount_in = amount * token_1_decimals;

    const price_diff = (amount_in * q96) / liquidty;

    const price_next = price_diff + current_sqrt_price;

    const output = calc_amount0(
      liquidty,
      price_next,
      Number(current_sqrt_price)
    );
    return output / token_0_decimals;
  }
}

*Примечание: рекомендуется использовать тип данных BigInt(), чтобы обеспечить точные расчеты выходной суммы. Имейте в виду, что приведенные здесь формулы являются общим обзором.*

В зависимости от того, продаем ли мы токен 0 или токен 1, этот метод использует два разных подхода для определения суммы вывода по сделке. Тем не менее, оба подхода следуют одному и тому же набору рекомендаций, которые включают в себя определение изменения стоимости, определение квадратного корня из целевой цены и расчет соответствующего объема выпуска токена.

После того, как мы выполнили итерацию по каждому пулу ликвидности и обновили нашу торговую сумму, мы вычисляем процентную разницу в цене между нашей начальной суммой ΔY и нашей конечной суммой ΔX, используя простую формулу для расчета процентной разницы в цене.

Наконец, после того, как мы рассчитали каждую перестановку, возвращается самая прибыльная вместе с процентной разницей в цене данной перестановки.

II. График: поиск кредитных пулов

Чтобы успешно интегрировать быстрые кредиты, мы должны найти оптимальных кредиторов с достаточной ликвидностью и низкой комиссией за обмен. Мы можем создавать запросы с определенными параметрами для поиска таких пулов на основе схем наших подграфов. Uniswap V3 — лучший вариант из-за его концентрированной ликвидности, но мы также можем использовать Uniswap V2 и Sushiswap в качестве запасных вариантов. Мы используем две сборки GraphQL, чтобы вернуть единый пул ликвидности, соответствующий нашим стандартам, и получить UUID LP.

{
   pairs(first: 1, orderBy: reserveUSD, orderDirection: desc, where :{${token_number}:"${tokenID}", id_not_in: ["${poolAddress}", "${poolAddress2}", "${poolAddress3}"], reserveUSD_gt: 0}) { 
    id
    reserve0
    reserve1
    reserveUSD
    token0Price
    token1Price
    token0 {
      symbol
      id
      decimals
      derivedETH
    }
    token1 {
      symbol
      id
      decimals
      derivedETH
    }
   }
  }

Здесь мы находим пулы ликвидности в Uniswap V2 и Sushiswap с наибольшим «резервным долларом США», содержащим введенный UUID токена. Поскольку комиссия за обмен для этих DEX составляет фиксированные 3%, эти запросы нацелены исключительно на пулы с достаточной ликвидностью.

{
   token(id: "${tokenID}"){
    whitelistPools(first: 1, orderBy: liquidity, orderDirection: asc, where: {liquidity_gt: "0", tick_gt: 0, id_not_in: ["${poolAddress1}", "${poolAddress2}", "${poolAddress3}"], feeTier_lte: "3000"}){
      token0 {
        symbol
        decimals
        id
        derivedETH
      }
      token0Price
      token1Price
      token1 {
        symbol
        decimals
        id
        derivedETH
      }
      feeTier
      liquidity
      tick
      sqrtPrice
      id
    }
   }
  }

Наш второй запрос уникален для Uniswap V3 и направлен на поиск пулов ликвидности с низкой комиссией за своп в диапазоне от 3% до 0,01%. По этим параметрам мы также отфильтровываем пулы с низкой ликвидностью.

Мы используем полученный UUID, чтобы найти самое последнее событие обмена для пула ликвидности, что помогает нам рассчитать текущую оценку токенов (включая оценку в долларах США) и создать библиотеку пула ликвидности для нашего метода определения прибыльности.

// ---- uniswap V2 and sushiswap query -----// 
{
  {
    swaps(first: 1, orderBy: timestamp, orderDirection: desc, where: { pair: "${address}" } ) {
      pair {
        token0 {
          symbol
          id
          decimals
        }
        token1 {
          symbol
          id
          decimals
        }
        token0Price
        token1Price
        reserve0
        reserve1
        id
      }
      amount0In
      amount0Out
      amount1In
      amount1Out
      amountUSD
      to
      timestamp
    }
  }
}
// ---- uniswap V3 query -----//

 {
    swaps(first: 1, orderBy: timestamp, orderDirection: desc, where: {pool: "${address}"}){
      sqrtPriceX96
      amountUSD
      timestamp
      id
      amount0
      amount1
      pool {
        token0{
          symbol
          decimals
          id
          derivedETH
        }
        token0Price
        token1Price
        token1 {
          symbol
          decimals
          id
          derivedETH
        }
        feeTier
        liquidity
        tick
        sqrtPrice
        id
      }
    }
  }

get_loan_pool_for_token() — определяет порядок, в котором мы ищем кредитные пулы на разных биржах. Это делается путем вызова соответствующих функций подграфа в указанном порядке.

async function get_loan_pool_for_token(tokenId, poolAddresses) {
  try {
    const [poolAddress1, poolAddress2, poolAddress3] = poolAddresses;

    const uniswap_v3_loan_pool_id = await find_most_profitable_loan_pool_V3(
      tokenId,
      poolAddress1,
      poolAddress2,
      poolAddress3
    );

    if (uniswap_v3_loan_pool_id) {
      return await get_uniswap_v3_last_swap_information(
        uniswap_v3_loan_pool_id
      );
    }

    const uniswap_v2_loan_pool_id = await find_most_profitable_loan_pool_V2(
      tokenId,
      poolAddress1,
      poolAddress2,
      poolAddress3
    );
    if(uniswap_v2_loan_pool_id){
      return await get_most_recent_swap_activity_uniswapV2(
        uniswap_v2_loan_pool_id
      );
    }
    const sushi_swap_loan_pool_id =
      await find_most_profitable_loan_pool_sushi_swap(
        tokenId,
        poolAddress1,
        poolAddress2,
        poolAddress3
      );

    if(sushi_swap_loan_pool_id) {
      return await get_most_recent_swap_activity_sushiswap(
        sushi_swap_loan_pool_id
      );
    }


  } catch (error) {
    console.error(error);
  }
}

Когда мы не можем найти какие-либо кредитные пулы для определенного токена в наших подграфах DEX, мы исключаем его из рассмотрения. Тем не менее, это решение несет в себе риск потенциального убытка от выгодных перестановок, которые могут возникнуть в будущем.

III. Определение рентабельности

Процесс оценки прибыльности нашего пути состоит из двух отдельных проверок. Первая — проверка вне сети, где мы определяем оптимальное количество входных токенов и наиболее выгодную перестановку пути. Второй — проверка в цепочке, которая использует ABI пулов ликвидности для имитации сделки с использованием текущих рыночных данных.

async function profitablity_checks(mapped_paths) {

  const ordered_profitable_path_with_optimal_input_amount = [];

  const proftibale_paths_to_stage_for_smart_contract = [];

  /**
   * off chain check
   */
  for (const path of mapped_paths) {

    off_chain_check(path);
   
    if (path.profit_usd > MIN_PROFIT_TO_CONSIDER_FOR_ON_CHAIN_CALL) {
      ordered_profitable_path_with_optimal_input_amount.push(path);
    }
  }
  /**
   * on chain check our final vet cycle
   */
  for (const final_path of ordered_profitable_path_with_optimal_input_amount) {

    await on_chain_check(final_path);

    if (
      final_path.profit_usd_onchain_check >
      MIN_PROFIT_TO_CONSIDER_FOR_ON_CHAIN_CALL
    ) {
      proftibale_paths_to_stage_for_smart_contract.push(final_path);
    }
  }
  return proftibale_paths_to_stage_for_smart_contract;
}

После того, как путь пройдет обе проверки, мы можем приступить к его подготовке для нашего смарт-контракта и исполнения мгновенного кредита.

Определение прибыльности: оптимальная сумма ввода токена

off_chain_check() — использует метод рекурсивной итерации, аналогичный нашему подходу к процентной разнице цен, для расчета каждой сделки на основе обмена пула ликвидности для каждой перестановки пути. Однако эта функция отличается, например, тем, как она определяет оптимальную сумму ввода, рассчитывает каждую сделку на основе комиссий за обмен и оценивает прибыльность на основе текущей ликвидности.

Существуют различные способы определения оптимальной суммы ввода. Одна из возможностей состоит в том, чтобы использовать математический подход для нашей велосипедной дорожки.

Если мы сможем рассчитать результаты торговли, используя запасы токенов, мы сможем определить наиболее оптимальный ввод, перебирая наш путь и вычисляя производную от суммы ввода.

Наш метод, напротив, использует метод грубой силы. Мы используем generate_optimal_token_input_amounts() для создания массива заемных сумм путем расчета оценки заемных активов из нашего кредитного пула.

function generate_optimal_token_input_amounts(borrow_token_usd_price) {

  const token_input_amounts = [];

  for (
    let i = MIN_USD_VALUE_FOR_OPTIMAL_INPUT_RANGE;
    i <= MAX_USD_VALUE_FOR_OPTIMAL_INPUT_RANGE;
    i += STEP_BETWEEN_RANGE
  ) {

    const amount_in = i / borrow_token_usd_price;

    token_input_amounts.push([amount_in, i]);

  }

  return token_input_amounts;
}

После получения нашего массива заемных активов мы используем calculate_permutation_and_find_optimal_input_token_amount() для повторения каждой заемной суммы и расчета текущей перестановки пути. В этом процессе мы моделируем торговлю, используя те же методы расчета, что и в нашем методе PPD (ценовая разница в процентах), а также учитываем торговые сборы, удерживаемые каждым пулом ликвидности.

function calculate_permutation_and_find_optimal_input_token_amount(
  permutation_path,
  loan_pools
) {
  try {
    verfiy_token_path(permutation_path);

    const loan_pool = loan_pools[permutation_path[0].token_in];

    const calcuations = {
      optimal_amount: 0,
      profit: 0,
      usd_input_amount: 0,
      starting_amount: 0,
      path: permutation_path,
      enough_liquidity: true,
    };
    if (loan_pool) {
      const borrow_token_usd_price =
        permutation_path[0].token_in === loan_pool.token0.id
          ? loan_pool.token_0_usd_price
          : loan_pool.token_1_usd_price;

      const loan_fee = 1 - FEE_TEIR_PERCENTAGE_OBJECT[loan_pool.feeTier];

      const amounts = generate_optimal_token_input_amounts(
        borrow_token_usd_price
      );
      for (const [starting_amount, usd_input_amount] of amounts) {

        calcuations.enough_liquidity = true;

        let input_amount = starting_amount * loan_fee;

        const [pool_1, pool_2, pool_3] = permutation_path;

        input_amount = calculate_new_token_amount(
          pool_1,
          input_amount,
          calcuations
        );

        input_amount = calculate_new_token_amount(
          pool_2,
          input_amount,
          calcuations
        );
        input_amount = calculate_new_token_amount(
          pool_3,
          input_amount,
          calcuations
        );
        const profit =
          (input_amount - starting_amount) * borrow_token_usd_price;

        if (!calcuations.enough_liquidity) {
          return;
        }
        if (calcuations.profit === 0) {
          calcuations.profit = profit;
          calcuations.optimal_amount = starting_amount;
          calcuations.usd_input_amount = usd_input_amount;
        }

        if (calcuations.profit < profit) {
          calcuations.profit = profit;
          calcuations.optimal_amount = starting_amount;
          calcuations.usd_input_amount = usd_input_amount;
        } else if (calcuations.profit > profit) {
          return calcuations;
        }
      }
    }
  } catch (error) {
    console.error(error);
  }
}

В течение цикла итерации, если в какой-то момент ликвидность, предоставляемая одним из наших пулов, падает ниже нашей итоговой суммы, итерации для текущей перестановки заканчиваются. Возвращенный результат указывает на то, что торговая перестановка не имеет достаточной ликвидности, при условии, что предыдущие расчеты не показали прибыли.

Определение прибыльности: окончательная проверка в сети

На последнем этапе нашего процесса мы используем бинарные интерфейсы контрактных приложений пулов ликвидности (ABI). ABI определяет методы и переменные, содержащиеся в смарт-контракте, что позволяет нам взаимодействовать с контрактом и получать доступ к его функциям. Взаимодействуя с ABI, мы можем выполнять функции, которые позволяют нам совершать сделки и читать данные по цепочке.

Итак, как нам получить доступ к функциям смарт-контракта пула ликвидности?

Доступно несколько сервисов, таких как Инфура, Алхимия и Калейдо. Эти сервисы предлагают бесплатные аккаунты с лимитом запросов. После того, как мы создали учетную запись и получили ключ API, мы можем использовать такие библиотеки, как ethers.js или web3, чтобы получить доступ к ABI смарт-контрактов и взаимодействовать с их функциями.

const INFURA_URL_VETTING_KEY = // https://mainnet.infura.io/v3/---API KEY HERE-----  //

const provider = new ethers.providers.JsonRpcProvider(INFURA_URL_VETTING_KEY);

// the router address are specific to the DEX, you find these addresses with a
// simple google search. 

const uniswap_V3_contract = new ethers.Contract(
      ROUTER_ADDRESS_V3,
      abi,
      provider
    );

const uniswap_V2_contract = new ethers.Contract(
      ROUTER_ADDRESS_V2,
      abi,
      provider
    );

const sushiswap_contract = new ethers.Contract(
      ROUTER_ADDRESS_SUSHISWAP,
      abi,
      provider
    );

Цель наших вызовов данных в сети аналогична нашим предыдущим методам — мы хотим смоделировать сделку, чтобы оценить ее стоимость. Однако в данном случае мы используем оперативные данные для повышения точности наших расчетов.

Для достижения этой цели мы в первую очередь полагаемся на две статические функции. Эти функции обеспечивают точный и удобный способ оценки стоимости нашей картографической торговли. Используя оперативные данные из блокчейна, мы можем принимать более обоснованные решения и более эффективно совершать сделки. Этот подход обычно используется в децентрализованных биржах и других автоматизированных системах маркетмейкинга для облегчения сделок на основе рыночных условий в реальном времени.

quoteExactInputSingle() — используется для Uniswap V3, чтобы указать цену сделки, в которой задействован один входной актив. Он учитывает комиссионный фактор для пула, а также предел цены квадратного корня, который можно использовать для указания диапазона цен для сделки.

// ---- ABI function call uniswap V3 ---- //

  function quoteExactInputSingle(
    address tokenIn,
    address tokenOut,
    uint24 fee,
    uint256 amountIn,
    uint160 sqrtPriceLimitX96
  ) public returns (uint256 amountOut)

getAmountsOut() — уникальная функция Uniswap V2 и сушисвоп. Он использует собственные методы внутреннего контракта — getReserves() и getAmountOut() — для расчета максимального количества токенов вывода для каждого последующего шага на предопределенном пути.

// ---- ABI function call uniswap V2 and sushiswap ---- //

function getAmountsOut(
  uint amountIn, 
  address[] memory path
) public view returns (uint[] memory amounts);

Что дальше?

Мы изучили процесс выявления прибыльных возможностей треугольного арбитража и использования таких протоколов, как The Graph, для индексации основной сети Ethereum для получения соответствующих данных. Мы также углубились в применение математики CFMM в нашем скрипте, чтобы обнаружить такие возможности. Обнаружив жизнеспособную возможность треугольного арбитража, мы можем теперь перейти к последнему разделу, который посвящен созданию смарт-контракта и внедрению быстрых кредитов для выполнения сделок (скоро).

Получите полное представление о текущем состоянии нашей сборки: Репозиторий GitHub

Источники и дополнительная литература

Новичок в трейдинге? Попробуйте криптотрейдинговые боты или копи-трейдинг на лучших криптобиржах

Присоединяйтесь к Coinmonks Telegram Channel и Youtube Channel и получайте ежедневные Крипто новости

Также читайте