Construindo um robô de arbitragem: Encontrando oportunidades de arbitragem

intermediário4/9/2024, 2:29:22 PM
Neste artigo, realizamos uma pré-seleção de pares de tokens de interesse. Em seguida, derivamos a fórmula matemática para encontrar a arbitragem ótima entre dois pools dos mesmos pares de tokens.

Se a sua configuração de MEV não se parece com isso, você está ngmi

Este artigo faz parte de uma série sobre a construção de um bot de arbitragem. O objetivo desta série é fornecer um guia passo a passo para a construção de um robô de negociação MEV automatizado que pode encontrar e executar oportunidades de arbitragem em trocas descentralizadas populares.

Neste artigo, realizamos uma pré-seleção de pares de tokens de interesse. Em seguida, derivamos a fórmula matemática para encontrar a arbitragem ótima entre dois pools dos mesmos pares de tokens. Finalmente, implementamos a fórmula em código e retornamos uma lista de oportunidades potenciais de arbitragem.

Selecionando os pares de tokens

Precisões sobre a estratégia de arbitragem

Antes de começarmos a procurar oportunidades de arbitragem, precisamos definir claramente o perímetro do nosso robô de arbitragem. Especificamente, que tipo de arbitragens queremos atuar. O tipo mais seguro de arbitragem é entre pools que envolvem ETH. Como o ETH é o ativo com o qual o gás de nossas transações é pago, é natural sempre querer acabar com ETH após uma arbitragem. Mas todos são tentados a pensar assim. Lembre-se de que, no trading, oportunidades pontuais se tornam menos lucrativas à medida que mais pessoas atuam sobre elas.

Por uma questão de simplicidade, vamos nos concentrar nas oportunidades de arbitragem entre pools que envolvem ETH. Vamos procurar apenas oportunidades entre dois pools do mesmo par de tokens. Não iremos negociar oportunidades que envolvam mais de 2 pools na rota de negociação (as chamadas oportunidades de várias etapas). Observe que aprimorar esta estratégia para uma mais arriscada é o primeiro passo que você deve tomar para melhorar a lucratividade do seu robô.

Para melhorar essa estratégia, você poderia, por exemplo, manter parte do inventário em stablecoins e aproveitar oportunidades de arbitragem que gerem stablecoins. O mesmo poderia ser feito para ativos muito mais arriscados, como shitcoins (com as devidas precauções), e reequilibrar periodicamente sua carteira em ETH para pagar pelo gás.

Outra direção seria abandonar a suposição implícita de atomicidade que fizemos e introduzir raciocínio estatístico em nossa estratégia. Por exemplo, comprando um token em um pool quando o preço se moveu favoravelmente mais do que alguma quantidade de desvios padrão, e vendendo mais tarde (estratégia de reversão à média). Isso seria ideal para shitcoins que não estão listadas em bolsas centralizadas muito mais eficientes, ou aquelas que estão, mas cujo preço não é rastreado corretamente on-chain. Isso envolve muitas peças móveis e está fora do escopo desta série.

Selecionando os pares de tokens

Agora que definimos o perímetro do nosso robô de arbitragem, precisamos selecionar os pares de tokens nos quais queremos negociar. Aqui estão os 2 critérios de seleção que iremos usar:

  • Os pares selecionados devem envolver ETH.
  • Os pares precisam ser negociados em pelo menos 2 pools diferentes.

Reutilizando o código de artigo 2: Leitura eficiente dos preços da pool, temos o seguinte código que lista todos os pares de tokens que foram implantados pelos contratos da fábrica fornecidos:

# [...]# Carregar os endereços do contrato da fábricacom open("FactoriesV2.json", "r") as f:fábricas = json.load(f)# [...]# Buscar lista de pools para cada contrato da fábricapairDataList = []for nomeFábrica, dadosFábrica in fábricas.items():eventos = getPairEvents(w3.eth.contract(address=dadosFábrica['factory'], abi=factory_abi), 0, w3.eth.block_number)print(f'Encontradas {len(eventos)} pools para {nomeFábrica}')for e in eventos:   pairDataList.append({       "token0": e["args"]["token0"],       "token1": e["args"]["token1"],       "pair": e["args"]["pair"],       "fábrica": nomeFábrica   })

Vamos simplesmente inverter pairDataList em um dicionário onde as chaves são os pares de tokens e os valores são a lista de pools que negociam esse par. Ao percorrer a lista, ignoramos os pares que não envolvem ETH. Quando o loop terminar, os pares com pelo menos 2 pools selecionados serão armazenados em listas com pelo menos 2 elementos:

# [...]WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"pair_pool_dict = {}for pair_object in pairDataList:# Check for ETH (WETH) in the pair.pair = (pair_object['token0'], pair_object['token1'])if WETH not in pair:   continue# Make sure the pair is referenced in the dictionary. if pair not in pair_pool_dict:   pair_pool_dict[pair] = []# Add the pool to the list of pools that trade this pair.pair_pool_dict[pair].append(pair_object)# Create the final dictionnary of pools that will be traded on.pool_dict = {}for pair, pool_list in pair_pool_dict.items():if len(pool_list) >= 2:   pool_dict[pair] = pool_list

Algumas estatísticas devem ser impressas para ter um melhor controle dos dados com os quais estamos trabalhando:

# Número de pares diferentesprint(f'Temos {len(pool_dict)} pares diferentes.')# Número total de poolsprint(f'Temos {sum([len(pool_list) for pool_list in pool_dict.values()])} pools no total.')# Par com mais pools print(f'O par com mais pools é {max(pool_dict, key=lambda k: len(pool_dict[k]))} com {len(max(pool_dict.values(), key=len))} pools.')# Distribuição do número de pools por par, decilspool_count_list = [len(pool_list) for pool_list in pool_dict.values()]pool_count_list.sort(reverse=True)print(f'Número de pools por par, em decil: {pool_count_list[::int(len(pool_count_list)/10)]}')# Distribuição do número de pools por par, percentis (decis do primeiro decil)pool_count_list.sort(reverse=True)print(f'Número de pools por par, em percentis: {pool_count_list[::int(len(pool_count_list)/100)][:10]}')

No momento da escrita, isso produz o seguinte:

Temos 1431 pares diferentes.

Temos 3081 pools no total.

O par com mais pools é ('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', '0xdAC17F958D2ee523a2206206994597C13D831ec7') com 16 pools.

Número de pools por par, em décimos: [16, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]

Número de pools por par, em percentis: [16, 5, 4, 3, 3, 3, 3, 3, 3, 3]

Obter reservas para 3000 pools pode ser feito em menos de 1 segundo com nós RPC públicos. Este é um tempo razoável.

Agora, que temos todos os dados de que precisamos, precisamos começar a encontrar oportunidades de arbitragem.

Encontrando oportunidades de arbitragem

Idea geral

Existe uma oportunidade de arbitragem sempre que houver uma discrepância de preço entre dois pools que negociam o mesmo par. No entanto, nem todas as diferenças de preço são exploráveis: o custo de gás da transação estabelece um valor mínimo que deve ser recuperado pelo trade, e a liquidez em cada pool limita o valor que pode ser extraído de uma diferença de preço específica.

Para encontrar a oportunidade de arbitragem mais lucrativa acessível a nós, precisaremos calcular o valor potencial extraível de cada diferença de preço, considerando as reservas/liquidez em cada pool, e estimar o custo de gás da transação.

Fórmula de tamanho de negociação ótima de arbitragem

Quando uma oportunidade de arbitragem é explorada, o preço da pool que compra o token de entrada vai cair, e o preço da pool que vende vai subir. O movimento dos preços é descrito pela fórmula do produto constante.

Já vimos em@emileamajar/construindo-um-bot-de-arbitragem-criadores-de-mercado-automatizados-e-uniswap-2d208215d8c2">artigo 1 como computar a saída de uma troca através de um pool, dado as reservas desse pool e a quantidade de entrada.

Para encontrar o tamanho de negociação ótimo, primeiro encontramos uma fórmula para a saída de duas trocas sucessivas, dada alguma quantia de entrada e as reservas dos dois pools envolvidos nas trocas.

Assumimos que a entrada da primeira troca está em token0 e a entrada da segunda troca está em token1, o que finalmente resulta em uma saída em token0.

Deixe x ser o valor de entrada, (a1, b1) as reservas do primeiro pool, e (a2, b2) as reservas do segundo pool. A taxa é a taxa cobrada pelos pools e é assumida como sendo a mesma para ambos os pools (na maioria das vezes 0,3%).

Definimos uma função que calcula a saída de uma troca, dada a entrada x e reservas (a, b):

f(x, a, b) = b(1 - a/(a + x(1-fee)))

Então sabemos que a saída da primeira troca é:

out1(x) = f(x, a1, b1)

out1(x) = b1(1 - a1/(a1 + x(1-fee)))

A saída da segunda troca é: (observe as variáveis de reserva trocadas)

out2(x) = f(out1(x), b2, a2)

out2(x) = f(f(x, a1, b1), b2, a2)

out2(x) = a2 (1 - b2/(b2 + f(x, a1, b1)(1-fee)))

out2(x) = a2(1 - b2/(b2 + b1) (1 - a1/(a1 + x (1-fee))) (1-taxa)))

Podemos plotar essa função usando desmosAo escolher os valores de reserva de forma que simulemos o primeiro pool tendo 1 ETH e 1750 USDC, e o segundo pool tendo 1340 USDC e 1 ETH, obtemos o seguinte gráfico:

Gráfico do lucro bruto do comércio como função do valor de entrada

Observe que realmente traçamos out2(x) - x, que é o lucro da negociação, menos o valor de entrada.

Graficamente, podemos ver que o tamanho de negociação ideal é de 0.0607 ETH de entrada, o que gera um lucro de 0.0085 ETH. O contrato deve ter pelo menos 0.0607 ETH de liquidez em WETH para poder aproveitar essa oportunidade.

Este valor de lucro de 0.0085 ETH (~$16 ao escrever este artigo) NÃO é o lucro final da negociação, pois ainda precisamos levar em consideração o custo do gás da transação. Isso será discutido em um próximo artigo.

Queremos calcular automaticamente esse tamanho de negociação ideal para nosso robô MEV. Isso pode ser feito através de cálculo elementar. Temos uma função de uma variável x que queremos maximizar. A função atinge seu máximo para um valor de x onde a derivada da função é 0.

Vários ferramentas gratuitas e online podem ser usadas para calcular simbolicamente a derivada de uma função, como wolfram alpha.

Encontrando a derivada de nossa função de lucro bruto.

Encontrar tal derivada é muito simples com o Wolfram Alpha. Você também pode fazê-lo manualmente se não se sentir seguro sobre suas habilidades matemáticas.

Wolfram Alpha produz a seguinte derivada:

dout2(x)/dx = (a1b1a2b2(1-fee)^2)/(a1b2 + (1-fee)x(b1(1-fee)+b2))^2

Uma vez que queremos encontrar o valor de x que maximiza o lucro (que é out2(x) - x), precisamos encontrar o valor de x onde a derivada é 1 (e não 0).

Wolfram Alpha fornece a seguinte solução para x na equação dout2(x)/dx = 1:

x = (sqrt(a1b1a2b2(1-taxa)^4 (b1(1-fee)+b2)^2) - a1b2(1-fee)(b1(1-fee)+b2)) / ((1-fee)(b1(1-fee) + b2))^2

Com os valores das reservas que usamos no gráfico acima, obtemos x_optimal = 0.0607203782551, o que valida nossa fórmula (em comparação com o valor do gráfico de 0.0607).

Embora esta fórmula não seja muito legível, é fácil de implementar em código. Aqui está uma implementação em Python da fórmula para calcular a saída das 2 swaps e o tamanho de negociação ideal:

# Funções auxiliares para calcular o tamanho de negociação ideal# Saída de uma única troca:def saída_troca(x, a, b, taxa=0.003):return b * (1 - a/(a + x*(1-taxa)))# Lucro bruto de duas trocas sucessivas:def lucro_negociação(x, reservas1, reservas2, taxa=0.003): a1, b1 = reservas1a2, b2 = reservas2return saída_troca(saída_troca(x, a1, b1, taxa), b2, a2, taxa) - x# Quantidade de entrada idealdef tamanho_negociação_ideal(reservas1, reservas2, taxa=0.003):a1, b1 = reservas1a2, b2 = reservas2return (math.sqrt(a1*b1*a2*b2*(1-taxa)**4 * (b1*(1-taxa)+b2)**2) - a1*b2*(1-taxa)*(b1*(1-taxa)+b2)) / ((1-taxa) * (b1*(1-taxa) + b2))**2

Localizador de oportunidades de arbitragem

Agora que sabemos como calcular o lucro bruto de uma oportunidade de arbitragem entre quaisquer dois pools dados do mesmo par de tokens, simplesmente temos que iterar sobre todos os pares de tokens e testar dois a dois todos os pools que têm o mesmo par de tokens. Isso nos dará o lucro bruto de todas as possíveis oportunidades de arbitragem que estão dentro do perímetro de nossa estratégia.

Para estimar o lucro líquido de uma operação, precisamos estimar o custo do gás de explorar uma oportunidade dada. Isso pode ser feito precisamente simulando a transação através de um eth_call para um nó RPC, mas leva muito tempo e só pode ser feito para algumas dezenas de oportunidades por bloco.

Primeiro faremos uma estimativa bruta do custo do gás, assumindo um custo de gás de transação fixo (na verdade, um limite inferior), e eliminaremos as oportunidades que não são lucrativas o suficiente para cobrir o custo do gás. Somente então faremos uma estimativa precisa do custo do gás para as oportunidades restantes.

Aqui está o código que percorre todos os pares e todas as pools, e classifica as oportunidades por lucro:

# [...] # Buscar as reservas de cada pool em pool_dictto_fetch = [] # Lista de endereços de pool para os quais as reservas precisam ser buscadas.for par, pool_list em pool_dict.items():for pair_object em pool_list: to_fetch.append(pair_object["pair"]) # Adicione o endereço do poolprint(f"Fetching reserves of {len(to_fetch)} pools...")# getReservesParallel() é do artigo 2 no MEV bot seriesreserveList = asyncio.get_event_loop().run_until_complete(getReservesParallel(to_fetch,  providersAsync))# Construir lista de oportunidades de negociaçãoíndice = 0opps = []para par, pool_list em pool_dict.items():# Armazene as reservas nos objetos do pool para uso posteriorpara pair_object em pool_list: pair_object["reservas"] = reserveList[index] index += 1# Itere por todos os pools do par para poolA em pool_list: para poolB em pool_list: # Ignore se for o mesmo pool se poolA["pair"] == poolB["pair"]:            continue # Pule se uma das reservas for 0 (divisão por 0) se 0 em poolA["reservas"] ou 0 em poolB["reservas"]: continue # Reordene as reservas para que WETH seja sempre o primeiro token se poolA["token0"] == WETH: res_A = (poolA["reservas"][0], poolA["reservas"][1]) res_B = (poolB["reservas"][0], poolB["reservas"][1]) else: res_A = (poolA["reservas"][1],  poolA["reservas"][0]) res_B = (poolB["reservas"][1], poolB["reservas"][0]) # Calcular valor de entrada ótima através da fórmula x = optimal_trade_size(res_A, res_B) # Ignorar se a entrada ótima for negativa (a ordem dos pools é invertida) se x < 0: continuar # Calcular o lucro bruto em Wei (antes do custo do gás) lucro = trade_profit(x,  res_A, res_B) # Armazene detalhes da oportunidade. Os valores estão em ETH. (1e18 Wei = 1 ETH) opps.append({ "profit": lucro / 1e18, "input": x / 1e18, "pair": par, "poolA": poolA, "poolB": poolB, })print(f"Found {len(opps)} opportunities.")

Que produz a seguinte saída:

Obtendo reservas de 3081 piscinas.

Encontradas 1791 oportunidades.

Agora temos uma lista de todas as oportunidades. Só precisamos estimar seu lucro. Agora, simplesmente assumiremos um custo de gás constante para negociar em uma oportunidade.

Devemos usar um limite inferior para o custo de gás de uma troca na Uniswap V2. Experimentalmente, descobrimos que esse valor está perto de 43k de gás.

Explorar uma oportunidade requer 2 trocas, e executar uma transação no Ethereum custa um flat 21k de gás, totalizando 107k de gás por oportunidade.

Aqui está o código que calcula o lucro líquido estimado de cada oportunidade:

# [...]# Use o custo de gás codificado de 107k gás por oportunidade gp = w3.eth.gas_pricefor opp em opps:opp["net_profit"] = opp["profit"] - 107000 * gp / 1e18# Ordenar por lucro líquido estimadoopps.sort(key=lambda x: x["net_profit"], reverse=True)# Manter oportunidades positivaspositive_opps = [opp for opp in opps if opp["net_profit"] > 0]

Imprimir estatísticas

# Contagem de oportunidades positivasprint(f"Encontradas {len(positive_opps)} oportunidades positivas.")# Detalhes de cada oportunidade ETH_PRICE = 1900 # Você deve buscar dinamicamente o preço do ETHfor opp in positive_opps:print(f"Lucro: {opp['net_profit']} ETH (${opp['net_profit'] * ETH_PRICE})")print(f"Entrada: {opp['input']} ETH (${opp['input'] * ETH_PRICE})")print(f"Pool A: {opp['poolA']['pair']}")print(f"Pool B: {opp['poolB']['pair']}")print()

Aqui está a saída do script:

Encontradas 57 oportunidades positivas.

Lucro: 4.936025725859028 ETH ($9378.448879132153)

Input: 1.7958289984719014 ETH ($3412.075097096613)

Pool A: 0x1498bd576454159Bb81B5Ce532692a8752D163e8

Pool B: 0x7D7E813082eF6c143277c71786e5bE626ec77b20

{‘profit’: 4.9374642090282865, ‘input’: 1.7958(…)

Lucro: 4.756587769768892 ETH ($9037.516762560894)

Input: 0.32908348765283796 ETH ($625.2586265403921)

Pool A: 0x486c1609f9605fA14C28E311b7D708B0541cd2f5

Pool B: 0x5e81b946b61F3C7F73Bf84dd961dE3A0A78E8c33

{'profit': 4.7580262529381505, 'input': 0.329(...)}

Lucro: 0.8147203063054365 ETH ($1547.9685819803292)

Entrada: 0.6715171730669338 ETH ($1275.8826288271744)

Pool A: 0x1f1B4836Dde1859e2edE1C6155140318EF5931C2

Pool B: 0x1f7efDcD748F43Fc4BeAe6897e5a6DDd865DcceA

{‘lucro’: 0.8161587894746954, ‘entrada’: 0.671(…)

(…)

Qualquer lucro que seja suspeitosamente alto. O primeiro passo que deve ser tomado é verificar se o código está correto. Depois de verificar cautelosamente o código, descobrimos que o código está correto.

Esses lucros são reais? Acontece que não. Lançamos nossa rede muito ampla ao selecionar quais pools considerar em nossa estratégia e acabamos com pools de tokens tóxicos em nossas mãos.

O padrão de token ERC20 apenas descreve uma interface para interoperabilidade. Qualquer pessoa pode implantar um token que implementa essa interface e optar por implementar um comportamento não ortodoxo, que é exatamente o que está em jogo aqui.

Alguns criadores de tokens elaboram seu ERC20 de forma que os pools nos quais são negociados não possam vender, mas apenas comprar o token. Alguns contratos de tokens até possuem mecanismos de interruptor de segurança que permitem ao criador puxar o tapete de todos os seus usuários.

Em nosso bot de MEV, esses tokens tóxicos devem ser filtrados. Isso será abordado em um artigo futuro.

Se filtrarmos manualmente os tokens obviamente tóxicos, restarão as seguintes 42 oportunidades:

Lucro: 0.004126583158496902 ETH ($7.840508001144114)

Input: 0.008369804833786892 ETH ($15.902629184195094)

Pool A: 0xdF42388059692150d0A9De836E4171c7B9c09CBf

Pool B: 0xf98fCEB2DC0Fa2B3f32ABccc5e8495E961370B23

{‘profit’: 0.005565066327755902, (…)

Lucro: 0.004092580415474992 ETH ($7.775902789402485)

Entrada: 0.014696360216108083 ETH ($27.92308441060536)

Pool A: 0xfDBFb4239935A15C2C348400570E34De3b044c5F

Pool B: 0x0F15d69a7E5998252ccC39Ad239Cef67fa2a9369

{‘profit’: 0.005531063584733992, (...)

Lucro: 0.003693235163284344 ETH ($7.017146810240254)

Entrada: 0.1392339178514088 ETH ($264.5444439176767)

Pool A: 0x2957215d0473d2c811A075725Da3C31D2af075F1

Pool B: 0xF110783EbD020DCFBA91Cd1976b79a6E510846AA

{‘profit’: 0.005131718332543344, (...)

Lucro: 0.003674128918827048 ETH ($6.980844945771391)

Entrada: 0.2719041848570484 ETH ($516.617951228392)

Pool A: 0xBa19343ff3E9f496F17C7333cdeeD212D65A8425

Pool B: 0xD30567f1d084f411572f202ebb13261CE9F46325

{‘profit’: 0.005112612088086048, (…)

(…)

Observe que, em geral, os lucros são inferiores ao valor de entrada necessário para executar a transação.

Esses lucros são muito mais razoáveis. Mas lembre-se de que ainda são lucros no melhor cenário possível, pois usamos uma estimativa muito rudimentar do custo de gás de cada oportunidade.

Em um artigo futuro, simularemos a execução de nosso comércio para obter um valor preciso do custo de gás de cada oportunidade.

Para simular a execução, precisamos primeiro desenvolver o contrato inteligente que executará a negociação. Este é o tema do próximo artigo.

Conclusão

Agora temos uma definição clara do perímetro do nosso bot de arbitragem de MEV.

Exploramos a teoria matemática por trás da estratégia de arbitragem e a implementamos em Python.

Agora temos uma lista de oportunidades potenciais de arbitragem, e precisamos simular sua execução para obter um valor final de lucro. Para fazer isso, precisamos ter nosso contrato inteligente de negociação pronto.

No próximo artigo, desenvolveremos um contrato inteligente em Solidity e simularemos nosso primeiro comércio de arbitragem.

Você pode encontrar o código completo no repositório do github associado a este artigo. O script é melhor executado em um notebook Jupyter.

Aviso:

  1. Este artigo é reproduzido a partir de [ médio], Todos os direitos autorais pertencem ao autor original [Emile Amajar]. Título original do artigo”Construindo um robô de arbitragem: Encontrando oportunidades de arbitragem (artigo 3/n)”, Se houver objeções a essa reimpressão, entre em contato com o Gate Learnequipe e eles vão lidar com isso prontamente.
  2. Isenção de responsabilidade: As visões e opiniões expressas neste artigo são exclusivamente do autor e não constituem qualquer conselho de investimento.
  3. As traduções do artigo para outros idiomas são feitas pela equipe Gate Learn. A menos que mencionado, copiar, distribuir ou plagiar os artigos traduzidos é proibido.

Construindo um robô de arbitragem: Encontrando oportunidades de arbitragem

intermediário4/9/2024, 2:29:22 PM
Neste artigo, realizamos uma pré-seleção de pares de tokens de interesse. Em seguida, derivamos a fórmula matemática para encontrar a arbitragem ótima entre dois pools dos mesmos pares de tokens.

Se a sua configuração de MEV não se parece com isso, você está ngmi

Este artigo faz parte de uma série sobre a construção de um bot de arbitragem. O objetivo desta série é fornecer um guia passo a passo para a construção de um robô de negociação MEV automatizado que pode encontrar e executar oportunidades de arbitragem em trocas descentralizadas populares.

Neste artigo, realizamos uma pré-seleção de pares de tokens de interesse. Em seguida, derivamos a fórmula matemática para encontrar a arbitragem ótima entre dois pools dos mesmos pares de tokens. Finalmente, implementamos a fórmula em código e retornamos uma lista de oportunidades potenciais de arbitragem.

Selecionando os pares de tokens

Precisões sobre a estratégia de arbitragem

Antes de começarmos a procurar oportunidades de arbitragem, precisamos definir claramente o perímetro do nosso robô de arbitragem. Especificamente, que tipo de arbitragens queremos atuar. O tipo mais seguro de arbitragem é entre pools que envolvem ETH. Como o ETH é o ativo com o qual o gás de nossas transações é pago, é natural sempre querer acabar com ETH após uma arbitragem. Mas todos são tentados a pensar assim. Lembre-se de que, no trading, oportunidades pontuais se tornam menos lucrativas à medida que mais pessoas atuam sobre elas.

Por uma questão de simplicidade, vamos nos concentrar nas oportunidades de arbitragem entre pools que envolvem ETH. Vamos procurar apenas oportunidades entre dois pools do mesmo par de tokens. Não iremos negociar oportunidades que envolvam mais de 2 pools na rota de negociação (as chamadas oportunidades de várias etapas). Observe que aprimorar esta estratégia para uma mais arriscada é o primeiro passo que você deve tomar para melhorar a lucratividade do seu robô.

Para melhorar essa estratégia, você poderia, por exemplo, manter parte do inventário em stablecoins e aproveitar oportunidades de arbitragem que gerem stablecoins. O mesmo poderia ser feito para ativos muito mais arriscados, como shitcoins (com as devidas precauções), e reequilibrar periodicamente sua carteira em ETH para pagar pelo gás.

Outra direção seria abandonar a suposição implícita de atomicidade que fizemos e introduzir raciocínio estatístico em nossa estratégia. Por exemplo, comprando um token em um pool quando o preço se moveu favoravelmente mais do que alguma quantidade de desvios padrão, e vendendo mais tarde (estratégia de reversão à média). Isso seria ideal para shitcoins que não estão listadas em bolsas centralizadas muito mais eficientes, ou aquelas que estão, mas cujo preço não é rastreado corretamente on-chain. Isso envolve muitas peças móveis e está fora do escopo desta série.

Selecionando os pares de tokens

Agora que definimos o perímetro do nosso robô de arbitragem, precisamos selecionar os pares de tokens nos quais queremos negociar. Aqui estão os 2 critérios de seleção que iremos usar:

  • Os pares selecionados devem envolver ETH.
  • Os pares precisam ser negociados em pelo menos 2 pools diferentes.

Reutilizando o código de artigo 2: Leitura eficiente dos preços da pool, temos o seguinte código que lista todos os pares de tokens que foram implantados pelos contratos da fábrica fornecidos:

# [...]# Carregar os endereços do contrato da fábricacom open("FactoriesV2.json", "r") as f:fábricas = json.load(f)# [...]# Buscar lista de pools para cada contrato da fábricapairDataList = []for nomeFábrica, dadosFábrica in fábricas.items():eventos = getPairEvents(w3.eth.contract(address=dadosFábrica['factory'], abi=factory_abi), 0, w3.eth.block_number)print(f'Encontradas {len(eventos)} pools para {nomeFábrica}')for e in eventos:   pairDataList.append({       "token0": e["args"]["token0"],       "token1": e["args"]["token1"],       "pair": e["args"]["pair"],       "fábrica": nomeFábrica   })

Vamos simplesmente inverter pairDataList em um dicionário onde as chaves são os pares de tokens e os valores são a lista de pools que negociam esse par. Ao percorrer a lista, ignoramos os pares que não envolvem ETH. Quando o loop terminar, os pares com pelo menos 2 pools selecionados serão armazenados em listas com pelo menos 2 elementos:

# [...]WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"pair_pool_dict = {}for pair_object in pairDataList:# Check for ETH (WETH) in the pair.pair = (pair_object['token0'], pair_object['token1'])if WETH not in pair:   continue# Make sure the pair is referenced in the dictionary. if pair not in pair_pool_dict:   pair_pool_dict[pair] = []# Add the pool to the list of pools that trade this pair.pair_pool_dict[pair].append(pair_object)# Create the final dictionnary of pools that will be traded on.pool_dict = {}for pair, pool_list in pair_pool_dict.items():if len(pool_list) >= 2:   pool_dict[pair] = pool_list

Algumas estatísticas devem ser impressas para ter um melhor controle dos dados com os quais estamos trabalhando:

# Número de pares diferentesprint(f'Temos {len(pool_dict)} pares diferentes.')# Número total de poolsprint(f'Temos {sum([len(pool_list) for pool_list in pool_dict.values()])} pools no total.')# Par com mais pools print(f'O par com mais pools é {max(pool_dict, key=lambda k: len(pool_dict[k]))} com {len(max(pool_dict.values(), key=len))} pools.')# Distribuição do número de pools por par, decilspool_count_list = [len(pool_list) for pool_list in pool_dict.values()]pool_count_list.sort(reverse=True)print(f'Número de pools por par, em decil: {pool_count_list[::int(len(pool_count_list)/10)]}')# Distribuição do número de pools por par, percentis (decis do primeiro decil)pool_count_list.sort(reverse=True)print(f'Número de pools por par, em percentis: {pool_count_list[::int(len(pool_count_list)/100)][:10]}')

No momento da escrita, isso produz o seguinte:

Temos 1431 pares diferentes.

Temos 3081 pools no total.

O par com mais pools é ('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', '0xdAC17F958D2ee523a2206206994597C13D831ec7') com 16 pools.

Número de pools por par, em décimos: [16, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]

Número de pools por par, em percentis: [16, 5, 4, 3, 3, 3, 3, 3, 3, 3]

Obter reservas para 3000 pools pode ser feito em menos de 1 segundo com nós RPC públicos. Este é um tempo razoável.

Agora, que temos todos os dados de que precisamos, precisamos começar a encontrar oportunidades de arbitragem.

Encontrando oportunidades de arbitragem

Idea geral

Existe uma oportunidade de arbitragem sempre que houver uma discrepância de preço entre dois pools que negociam o mesmo par. No entanto, nem todas as diferenças de preço são exploráveis: o custo de gás da transação estabelece um valor mínimo que deve ser recuperado pelo trade, e a liquidez em cada pool limita o valor que pode ser extraído de uma diferença de preço específica.

Para encontrar a oportunidade de arbitragem mais lucrativa acessível a nós, precisaremos calcular o valor potencial extraível de cada diferença de preço, considerando as reservas/liquidez em cada pool, e estimar o custo de gás da transação.

Fórmula de tamanho de negociação ótima de arbitragem

Quando uma oportunidade de arbitragem é explorada, o preço da pool que compra o token de entrada vai cair, e o preço da pool que vende vai subir. O movimento dos preços é descrito pela fórmula do produto constante.

Já vimos em@emileamajar/construindo-um-bot-de-arbitragem-criadores-de-mercado-automatizados-e-uniswap-2d208215d8c2">artigo 1 como computar a saída de uma troca através de um pool, dado as reservas desse pool e a quantidade de entrada.

Para encontrar o tamanho de negociação ótimo, primeiro encontramos uma fórmula para a saída de duas trocas sucessivas, dada alguma quantia de entrada e as reservas dos dois pools envolvidos nas trocas.

Assumimos que a entrada da primeira troca está em token0 e a entrada da segunda troca está em token1, o que finalmente resulta em uma saída em token0.

Deixe x ser o valor de entrada, (a1, b1) as reservas do primeiro pool, e (a2, b2) as reservas do segundo pool. A taxa é a taxa cobrada pelos pools e é assumida como sendo a mesma para ambos os pools (na maioria das vezes 0,3%).

Definimos uma função que calcula a saída de uma troca, dada a entrada x e reservas (a, b):

f(x, a, b) = b(1 - a/(a + x(1-fee)))

Então sabemos que a saída da primeira troca é:

out1(x) = f(x, a1, b1)

out1(x) = b1(1 - a1/(a1 + x(1-fee)))

A saída da segunda troca é: (observe as variáveis de reserva trocadas)

out2(x) = f(out1(x), b2, a2)

out2(x) = f(f(x, a1, b1), b2, a2)

out2(x) = a2 (1 - b2/(b2 + f(x, a1, b1)(1-fee)))

out2(x) = a2(1 - b2/(b2 + b1) (1 - a1/(a1 + x (1-fee))) (1-taxa)))

Podemos plotar essa função usando desmosAo escolher os valores de reserva de forma que simulemos o primeiro pool tendo 1 ETH e 1750 USDC, e o segundo pool tendo 1340 USDC e 1 ETH, obtemos o seguinte gráfico:

Gráfico do lucro bruto do comércio como função do valor de entrada

Observe que realmente traçamos out2(x) - x, que é o lucro da negociação, menos o valor de entrada.

Graficamente, podemos ver que o tamanho de negociação ideal é de 0.0607 ETH de entrada, o que gera um lucro de 0.0085 ETH. O contrato deve ter pelo menos 0.0607 ETH de liquidez em WETH para poder aproveitar essa oportunidade.

Este valor de lucro de 0.0085 ETH (~$16 ao escrever este artigo) NÃO é o lucro final da negociação, pois ainda precisamos levar em consideração o custo do gás da transação. Isso será discutido em um próximo artigo.

Queremos calcular automaticamente esse tamanho de negociação ideal para nosso robô MEV. Isso pode ser feito através de cálculo elementar. Temos uma função de uma variável x que queremos maximizar. A função atinge seu máximo para um valor de x onde a derivada da função é 0.

Vários ferramentas gratuitas e online podem ser usadas para calcular simbolicamente a derivada de uma função, como wolfram alpha.

Encontrando a derivada de nossa função de lucro bruto.

Encontrar tal derivada é muito simples com o Wolfram Alpha. Você também pode fazê-lo manualmente se não se sentir seguro sobre suas habilidades matemáticas.

Wolfram Alpha produz a seguinte derivada:

dout2(x)/dx = (a1b1a2b2(1-fee)^2)/(a1b2 + (1-fee)x(b1(1-fee)+b2))^2

Uma vez que queremos encontrar o valor de x que maximiza o lucro (que é out2(x) - x), precisamos encontrar o valor de x onde a derivada é 1 (e não 0).

Wolfram Alpha fornece a seguinte solução para x na equação dout2(x)/dx = 1:

x = (sqrt(a1b1a2b2(1-taxa)^4 (b1(1-fee)+b2)^2) - a1b2(1-fee)(b1(1-fee)+b2)) / ((1-fee)(b1(1-fee) + b2))^2

Com os valores das reservas que usamos no gráfico acima, obtemos x_optimal = 0.0607203782551, o que valida nossa fórmula (em comparação com o valor do gráfico de 0.0607).

Embora esta fórmula não seja muito legível, é fácil de implementar em código. Aqui está uma implementação em Python da fórmula para calcular a saída das 2 swaps e o tamanho de negociação ideal:

# Funções auxiliares para calcular o tamanho de negociação ideal# Saída de uma única troca:def saída_troca(x, a, b, taxa=0.003):return b * (1 - a/(a + x*(1-taxa)))# Lucro bruto de duas trocas sucessivas:def lucro_negociação(x, reservas1, reservas2, taxa=0.003): a1, b1 = reservas1a2, b2 = reservas2return saída_troca(saída_troca(x, a1, b1, taxa), b2, a2, taxa) - x# Quantidade de entrada idealdef tamanho_negociação_ideal(reservas1, reservas2, taxa=0.003):a1, b1 = reservas1a2, b2 = reservas2return (math.sqrt(a1*b1*a2*b2*(1-taxa)**4 * (b1*(1-taxa)+b2)**2) - a1*b2*(1-taxa)*(b1*(1-taxa)+b2)) / ((1-taxa) * (b1*(1-taxa) + b2))**2

Localizador de oportunidades de arbitragem

Agora que sabemos como calcular o lucro bruto de uma oportunidade de arbitragem entre quaisquer dois pools dados do mesmo par de tokens, simplesmente temos que iterar sobre todos os pares de tokens e testar dois a dois todos os pools que têm o mesmo par de tokens. Isso nos dará o lucro bruto de todas as possíveis oportunidades de arbitragem que estão dentro do perímetro de nossa estratégia.

Para estimar o lucro líquido de uma operação, precisamos estimar o custo do gás de explorar uma oportunidade dada. Isso pode ser feito precisamente simulando a transação através de um eth_call para um nó RPC, mas leva muito tempo e só pode ser feito para algumas dezenas de oportunidades por bloco.

Primeiro faremos uma estimativa bruta do custo do gás, assumindo um custo de gás de transação fixo (na verdade, um limite inferior), e eliminaremos as oportunidades que não são lucrativas o suficiente para cobrir o custo do gás. Somente então faremos uma estimativa precisa do custo do gás para as oportunidades restantes.

Aqui está o código que percorre todos os pares e todas as pools, e classifica as oportunidades por lucro:

# [...] # Buscar as reservas de cada pool em pool_dictto_fetch = [] # Lista de endereços de pool para os quais as reservas precisam ser buscadas.for par, pool_list em pool_dict.items():for pair_object em pool_list: to_fetch.append(pair_object["pair"]) # Adicione o endereço do poolprint(f"Fetching reserves of {len(to_fetch)} pools...")# getReservesParallel() é do artigo 2 no MEV bot seriesreserveList = asyncio.get_event_loop().run_until_complete(getReservesParallel(to_fetch,  providersAsync))# Construir lista de oportunidades de negociaçãoíndice = 0opps = []para par, pool_list em pool_dict.items():# Armazene as reservas nos objetos do pool para uso posteriorpara pair_object em pool_list: pair_object["reservas"] = reserveList[index] index += 1# Itere por todos os pools do par para poolA em pool_list: para poolB em pool_list: # Ignore se for o mesmo pool se poolA["pair"] == poolB["pair"]:            continue # Pule se uma das reservas for 0 (divisão por 0) se 0 em poolA["reservas"] ou 0 em poolB["reservas"]: continue # Reordene as reservas para que WETH seja sempre o primeiro token se poolA["token0"] == WETH: res_A = (poolA["reservas"][0], poolA["reservas"][1]) res_B = (poolB["reservas"][0], poolB["reservas"][1]) else: res_A = (poolA["reservas"][1],  poolA["reservas"][0]) res_B = (poolB["reservas"][1], poolB["reservas"][0]) # Calcular valor de entrada ótima através da fórmula x = optimal_trade_size(res_A, res_B) # Ignorar se a entrada ótima for negativa (a ordem dos pools é invertida) se x < 0: continuar # Calcular o lucro bruto em Wei (antes do custo do gás) lucro = trade_profit(x,  res_A, res_B) # Armazene detalhes da oportunidade. Os valores estão em ETH. (1e18 Wei = 1 ETH) opps.append({ "profit": lucro / 1e18, "input": x / 1e18, "pair": par, "poolA": poolA, "poolB": poolB, })print(f"Found {len(opps)} opportunities.")

Que produz a seguinte saída:

Obtendo reservas de 3081 piscinas.

Encontradas 1791 oportunidades.

Agora temos uma lista de todas as oportunidades. Só precisamos estimar seu lucro. Agora, simplesmente assumiremos um custo de gás constante para negociar em uma oportunidade.

Devemos usar um limite inferior para o custo de gás de uma troca na Uniswap V2. Experimentalmente, descobrimos que esse valor está perto de 43k de gás.

Explorar uma oportunidade requer 2 trocas, e executar uma transação no Ethereum custa um flat 21k de gás, totalizando 107k de gás por oportunidade.

Aqui está o código que calcula o lucro líquido estimado de cada oportunidade:

# [...]# Use o custo de gás codificado de 107k gás por oportunidade gp = w3.eth.gas_pricefor opp em opps:opp["net_profit"] = opp["profit"] - 107000 * gp / 1e18# Ordenar por lucro líquido estimadoopps.sort(key=lambda x: x["net_profit"], reverse=True)# Manter oportunidades positivaspositive_opps = [opp for opp in opps if opp["net_profit"] > 0]

Imprimir estatísticas

# Contagem de oportunidades positivasprint(f"Encontradas {len(positive_opps)} oportunidades positivas.")# Detalhes de cada oportunidade ETH_PRICE = 1900 # Você deve buscar dinamicamente o preço do ETHfor opp in positive_opps:print(f"Lucro: {opp['net_profit']} ETH (${opp['net_profit'] * ETH_PRICE})")print(f"Entrada: {opp['input']} ETH (${opp['input'] * ETH_PRICE})")print(f"Pool A: {opp['poolA']['pair']}")print(f"Pool B: {opp['poolB']['pair']}")print()

Aqui está a saída do script:

Encontradas 57 oportunidades positivas.

Lucro: 4.936025725859028 ETH ($9378.448879132153)

Input: 1.7958289984719014 ETH ($3412.075097096613)

Pool A: 0x1498bd576454159Bb81B5Ce532692a8752D163e8

Pool B: 0x7D7E813082eF6c143277c71786e5bE626ec77b20

{‘profit’: 4.9374642090282865, ‘input’: 1.7958(…)

Lucro: 4.756587769768892 ETH ($9037.516762560894)

Input: 0.32908348765283796 ETH ($625.2586265403921)

Pool A: 0x486c1609f9605fA14C28E311b7D708B0541cd2f5

Pool B: 0x5e81b946b61F3C7F73Bf84dd961dE3A0A78E8c33

{'profit': 4.7580262529381505, 'input': 0.329(...)}

Lucro: 0.8147203063054365 ETH ($1547.9685819803292)

Entrada: 0.6715171730669338 ETH ($1275.8826288271744)

Pool A: 0x1f1B4836Dde1859e2edE1C6155140318EF5931C2

Pool B: 0x1f7efDcD748F43Fc4BeAe6897e5a6DDd865DcceA

{‘lucro’: 0.8161587894746954, ‘entrada’: 0.671(…)

(…)

Qualquer lucro que seja suspeitosamente alto. O primeiro passo que deve ser tomado é verificar se o código está correto. Depois de verificar cautelosamente o código, descobrimos que o código está correto.

Esses lucros são reais? Acontece que não. Lançamos nossa rede muito ampla ao selecionar quais pools considerar em nossa estratégia e acabamos com pools de tokens tóxicos em nossas mãos.

O padrão de token ERC20 apenas descreve uma interface para interoperabilidade. Qualquer pessoa pode implantar um token que implementa essa interface e optar por implementar um comportamento não ortodoxo, que é exatamente o que está em jogo aqui.

Alguns criadores de tokens elaboram seu ERC20 de forma que os pools nos quais são negociados não possam vender, mas apenas comprar o token. Alguns contratos de tokens até possuem mecanismos de interruptor de segurança que permitem ao criador puxar o tapete de todos os seus usuários.

Em nosso bot de MEV, esses tokens tóxicos devem ser filtrados. Isso será abordado em um artigo futuro.

Se filtrarmos manualmente os tokens obviamente tóxicos, restarão as seguintes 42 oportunidades:

Lucro: 0.004126583158496902 ETH ($7.840508001144114)

Input: 0.008369804833786892 ETH ($15.902629184195094)

Pool A: 0xdF42388059692150d0A9De836E4171c7B9c09CBf

Pool B: 0xf98fCEB2DC0Fa2B3f32ABccc5e8495E961370B23

{‘profit’: 0.005565066327755902, (…)

Lucro: 0.004092580415474992 ETH ($7.775902789402485)

Entrada: 0.014696360216108083 ETH ($27.92308441060536)

Pool A: 0xfDBFb4239935A15C2C348400570E34De3b044c5F

Pool B: 0x0F15d69a7E5998252ccC39Ad239Cef67fa2a9369

{‘profit’: 0.005531063584733992, (...)

Lucro: 0.003693235163284344 ETH ($7.017146810240254)

Entrada: 0.1392339178514088 ETH ($264.5444439176767)

Pool A: 0x2957215d0473d2c811A075725Da3C31D2af075F1

Pool B: 0xF110783EbD020DCFBA91Cd1976b79a6E510846AA

{‘profit’: 0.005131718332543344, (...)

Lucro: 0.003674128918827048 ETH ($6.980844945771391)

Entrada: 0.2719041848570484 ETH ($516.617951228392)

Pool A: 0xBa19343ff3E9f496F17C7333cdeeD212D65A8425

Pool B: 0xD30567f1d084f411572f202ebb13261CE9F46325

{‘profit’: 0.005112612088086048, (…)

(…)

Observe que, em geral, os lucros são inferiores ao valor de entrada necessário para executar a transação.

Esses lucros são muito mais razoáveis. Mas lembre-se de que ainda são lucros no melhor cenário possível, pois usamos uma estimativa muito rudimentar do custo de gás de cada oportunidade.

Em um artigo futuro, simularemos a execução de nosso comércio para obter um valor preciso do custo de gás de cada oportunidade.

Para simular a execução, precisamos primeiro desenvolver o contrato inteligente que executará a negociação. Este é o tema do próximo artigo.

Conclusão

Agora temos uma definição clara do perímetro do nosso bot de arbitragem de MEV.

Exploramos a teoria matemática por trás da estratégia de arbitragem e a implementamos em Python.

Agora temos uma lista de oportunidades potenciais de arbitragem, e precisamos simular sua execução para obter um valor final de lucro. Para fazer isso, precisamos ter nosso contrato inteligente de negociação pronto.

No próximo artigo, desenvolveremos um contrato inteligente em Solidity e simularemos nosso primeiro comércio de arbitragem.

Você pode encontrar o código completo no repositório do github associado a este artigo. O script é melhor executado em um notebook Jupyter.

Aviso:

  1. Este artigo é reproduzido a partir de [ médio], Todos os direitos autorais pertencem ao autor original [Emile Amajar]. Título original do artigo”Construindo um robô de arbitragem: Encontrando oportunidades de arbitragem (artigo 3/n)”, Se houver objeções a essa reimpressão, entre em contato com o Gate Learnequipe e eles vão lidar com isso prontamente.
  2. Isenção de responsabilidade: As visões e opiniões expressas neste artigo são exclusivamente do autor e não constituem qualquer conselho de investimento.
  3. As traduções do artigo para outros idiomas são feitas pela equipe Gate Learn. A menos que mencionado, copiar, distribuir ou plagiar os artigos traduzidos é proibido.
Empieza ahora
¡Registrarse y recibe un bono de
$100
!