Construir um bot de arbitragem: Encontrar 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 esta, está ngmi

Este artigo é 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 bolsas 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. Por fim, 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, temos de definir claramente o perímetro do nosso bot de arbitragem. Especificamente, que tipo de arbitragens queremos realizar. O tipo de arbitragem mais seguro é entre pools que envolvem ETH. Uma vez que o ETH é o ativo com o qual o gás das nossas transações é pago, é natural querer sempre acabar com ETH após uma arbitragem. Mas todos são tentados a pensar assim. Tenha em mente que, no trading, oportunidades pontuais se tornam menos e menos lucrativas à medida que mais pessoas atuam sobre elas.

Para simplificar, vamos focar nas oportunidades de arbitragem entre pools que envolvem ETH. Apenas procuraremos oportunidades entre duas 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 multi-hop). Note que aprimorar esta estratégia para uma mais arriscada é o primeiro passo que deve dar para melhorar a rentabilidade do seu robô.

Para melhorar esta estratégia, poderia, por exemplo, manter algum inventário em stablecoins e atuar em oportunidades de arbitragem que rendam stablecoins. O mesmo poderia ser feito para ativos muito mais arriscados como shitcoins (com as precauções necessárias) e reequilibrar periodicamente a sua carteira em ETH para pagar o gás.

Outra direção seria abandonar a suposição implícita de atomicidade que fizemos e introduzir o 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 exchanges centralizadas muito mais eficientes, ou aquelas que estão, mas cujo preço não é rastreado corretamente na cadeia. Isso envolve muitas partes móveis e está fora do escopo desta série.

Seleção dos pares de tokens

Agora que definimos o perímetro do nosso bot 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 implementados 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)# [...]# Obter lista de pools para cada contrato de fábricapairDataList = []for factoryName, factoryData in fábricas.items():eventos = getPairEvents(w3.eth.contract(address=factoryData['factory'], abi=factory_abi), 0, w3.eth.block_number)print(f'Encontradas {len(eventos)} pools para {factoryName}')for e in eventos:   pairDataList.append({       "token0": e["args"]["token0"],       "token1": e["args"]["token1"],       "pair": e["args"]["pair"],       "fábrica": factoryName   })

Vamos simplesmente inverter pairDataList num 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 selecionadas serão armazenados em listas com pelo menos 2 elementos:

# [...]WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"pair_pool_dict = {}for pair_object in pairDataList:# Verifique se o ETH (WETH) está no par.pair = (pair_object['token0'], pair_object['token1'])if WETH not in pair:   continue# Certifique-se de que o par está referenciado no dicionário. if pair not in pair_pool_dict:   pair_pool_dict[pair] = []# Adicione o pool à lista de pools que negociam este par.pair_pool_dict[pair].append(pair_object)# Crie o dicionário final de pools que serão negociadas.pool_dict = {}for pair, pool_list in pair_pool_dict.items():if len(pool_list) >= 2:   pool_dict[pair] = pool_list

Alguns dados devem ser impressos para termos uma melhor compreensão dos dados com os quais estamos a trabalhar:

# 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, décimaspool_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 décimos: {pool_count_list[::int(len(pool_count_list)/10)]}')# Distribuição do número de pools por par, percentis (décimos do primeiro décimo)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]}')

Neste momento, isto gera o seguinte:

Temos 1431 pares diferentes.

Temos um total de 3081 pools.

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

Número de pools por par, em decilas: [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]

A obtenção de reservas para 3000 pools pode ser feita em menos de 1 segundo com nós RPC públicos. Este é um período de tempo razoável.

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

Encontrar oportunidades de arbitragem

Idea geral

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

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 irá descer, e o preço da pool que vende irá subir. O movimento dos preços é descrito pela fórmula do produto constante.

Já vimos em @emileamajar/construir-um-bot-de-arbitragem-criadores-de-mercado-automatizados-e-uniswap-2d208215d8c2">artigo 1 como calcular a saída de uma troca através de uma pool, dado os reservatórios dessa pool e a quantidade de entrada.

Para encontrar o tamanho de negociação ideal, primeiro encontramos uma fórmula para a saída de duas trocas sucessivas, dado um determinado montante de entrada e as reservas dos dois pools envolvidos nas trocas.

Assumimos que a entrada da primeira troca é em token0 e a entrada da segunda troca é em token1, o que finalmente resulta numa saída em token0.

Deixe x ser a quantidade de entrada, (a1, b1) as reservas da primeira pool e (a2, b2) as reservas da segunda pool. A taxa é a taxa cobrada pelas pools e assume-se que é a mesma para ambas as pools (na maior parte do tempo, 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-taxa)))

O resultado 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-taxa)))

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

Podemos plotar esta função usando desmosAo escolher os valores de reserva de forma a simular que o primeiro pool tem 1 ETH e 1750 USDC, e o segundo pool tem 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

Note que na verdade traçamos out2(x) - x, que é o lucro do negócio, menos o montante de entrada.

Graficamente, podemos ver que o tamanho de negociação ótimo é 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 explorar esta 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 conta o custo do gás da transação. Isso será discutido num artigo seguinte.

Queremos calcular automaticamente este tamanho de negociação ótimo para o nosso bot de MEV. Isto pode ser feito através de cálculo elemental. Temos uma função de uma variável x que queremos maximizar. A função atinge o seu máximo para um valor de x onde a derivada da função é 0.

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

Encontrar a derivada da 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-fee)^4 (b1(1-taxa)+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 (comparado 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 o resultado das duas trocas e o tamanho ótimo da negociação:

# Funções auxiliares para calcular o tamanho de negociação ideal# Saída de uma única troca de swapdef swap_output(x, a, b, taxa=0.003):return b * (1 - a/(a + x*(1-taxa)))# Lucro bruto de duas trocas sucessivasdef trade_profit(x, reservas1, reservas2, taxa=0.003): a1, b1 = reservas1a2, b2 = reservas2return swap_output(swap_output(x, a1, b1, taxa), b2, a2, taxa) - x# Quantidade de entrada idealdef optimal_trade_size(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 de 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 da nossa estratégia.

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

Primeiro faremos uma estimativa bruta do custo de gás, assumindo um custo fixo de transação de gás (um limite inferior, na verdade), e eliminaremos as oportunidades que não são suficientemente lucrativas para cobrir o custo de gás. Só então faremos uma estimativa precisa do custo de 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 in pool_list: to_fetch.append(pair_object["pair"]) # Adicione o endereço do poolprint(f"Buscando reservas de {len(to_fetch)} pools...")# getReservesParallel() é do artigo 2 da série de bots MEVRESERVEList = 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 posterior para pair_object em pool_list: pair_object["reserves"] = reserveList[index] index += 1# Itere sobre todos os pools do parefor poolA em pool_list: para poolB em pool_list: # Pule 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["reserves"] ou 0 em poolB["reserves"]: continue # Reordene as reservas para que WETH seja sempre o primeiro token se poolA["token0"] == WETH: res_A = (poolA["reserves"][0], poolA["reserves"][1]) res_B = (poolB["reserves"][0], poolB["reserves"][1]) else: res_A = (poolA["reserves"][1],  poolA["reserves"][0]) res_B = (poolB["reserves"][1], poolB["reserves"][0]) # Calcule o valor da entrada ótima através da fórmula x = optimal_trade_size(res_A, res_B) # Pule se a entrada ótima for negativa (a ordem dos pools é invertida) se x < 0: continue # Calcule o lucro bruto em Wei (antes do custo do gás) lucro = trade_profit(x,  res_A, res_B) # Armazene os detalhes da oportunidade. Os valores estão em ETH. (1e18 Wei = 1 ETH) opps.append({ "lucro": lucro / 1e18, "entrada": x / 1e18, "par": par, "poolA": poolA, "poolB": poolB, })print(f"Found {len(opps)} opportunities.")

Que produz a seguinte saída:

Obtendo reservas de 3081 pools.

Encontradas 1791 oportunidades.

Agora temos uma lista de todas as oportunidades. Apenas precisamos estimar o lucro delas. Neste momento, simplesmente assumiremos um custo de gás constante para negociar 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á próximo de 43k de gás.

Aproveitar uma oportunidade requer 2 swaps e a execução de uma transação na Ethereum custa um valor fixo de 21k de gás, num total de 107k de gás por oportunidade.

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

# [...]# Utilize o custo de gás codificado de 107k gás por oportunidade gp = w3.eth.gas_pricefor opp in 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 positivasa_imprimir(f"Encontradas {len(positive_opps)} oportunidades positivas.")# Detalhes sobre cada oportunidade ETH_PRICE = 1900 # Você deve buscar dinamicamente o preço do ETHpara opp em positive_opps:a_imprimir(f"Lucro: {opp['net_profit']} ETH (${opp['net_profit'] * ETH_PRICE})")a_imprimir(f"Entrada: {opp['input']} ETH (${opp['input'] * ETH_PRICE})")a_imprimir(f"Pool A: {opp['poolA']['pair']}")a_imprimir(f"Pool B: {opp['poolB']['pair']}")a_imprimir()

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)

Input: 0.6715171730669338 ETH ($1275.8826288271744)

Pool A: 0x1f1B4836Dde1859e2edE1C6155140318EF5931C2

Pool B: 0x1f7efDcD748F43Fc4BeAe6897e5a6DDd865DcceA

{'profit': 0.8161587894746954, 'input': 0.671(…)

(...)

Que lucros estranhamente altos. O primeiro passo a ser dado é verificar se o código está correto. Depois de verificar cuidadosamente o código, descobrimos que o código está correto.

Estes lucros são reais? Acontece que não. Lançamos a nossa rede demasiado ampla ao selecionar quais pools considerar na nossa estratégia e acabámos por obter pools de tokens tóxicos.

O padrão de token ERC20 apenas descreve uma interface para interoperabilidade. Qualquer pessoa pode implementar um token que segue esta interface e optar por implementar comportamentos não ortodoxos, que é exatamente o que está em jogo aqui.

Alguns criadores de tokens criam os seus ERC20 de forma a que as pools em que são negociados não possam vender, mas apenas comprar o token. Alguns contratos de tokens até têm mecanismos de botão de desligar que permitem ao criador retirar todos os utilizadores.

No nosso bot de MEV, estes tokens tóxicos devem ser filtrados. Isso será abordado num artigo futuro.

Se filtrarmos manualmente os tokens obviamente tóxicos, ficamos com 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)

Input: 0.014696360216108083 ETH ($27.92308441060536)

Pool A: 0xfDBFb4239935A15C2C348400570E34De3b044c5F

Pool B: 0x0F15d69a7E5998252ccC39Ad239Cef67fa2a9369

{‘profit’: 0.005531063584733992, (…)

Lucro: 0.003693235163284344 ETH ($7.017146810240254)

Input: 0.1392339178514088 ETH ($264.5444439176767)

Pool A: 0x2957215d0473d2c811A075725Da3C31D2af075F1

Pool B: 0xF110783EbD020DCFBA91Cd1976b79a6E510846AA

{‘profit’: 0.005131718332543344, (…)

Lucro: 0.003674128918827048 ETH ($6.980844945771391)

Input: 0.2719041848570484 ETH ($516.617951228392)

Pool A: 0xBa19343ff3E9f496F17C7333cdeeD212D65A8425

Pool B: 0xD30567f1d084f411572f202ebb13261CE9F46325

{‘profit’: 0.005112612088086048, (…)

(...)

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

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

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

Para simular a execução, primeiro precisamos desenvolver o contrato inteligente que irá 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 potenciais oportunidades de arbitragem e precisamos simular a sua execução para obter um valor final de lucro. Para isso, precisamos ter o nosso contrato inteligente de negociação pronto.

No próximo artigo, iremos desenvolver um contrato inteligente em Solidity e simular o nosso primeiro comércio de arbitragem.

Pode encontrar o código completo no repositório do github associado a este artigoO script é melhor executado num notebook Jupyter.

Aviso Legal:

  1. Este artigo é reproduzido a partir de [ médio], Todos os direitos de autor pertencem ao autor original [Emile Amajar]. Título original do artigo "Construir um bot de arbitragem: Encontrar oportunidades de arbitragem (artigo 3/n)", Se houver objeções a esta reimpressão, por favor entre em contato com o Gate Learnequipa, e eles tratarão dela prontamente.
  2. Aviso de responsabilidade: As 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. Salvo indicação em contrário, copiar, distribuir ou plagiar os artigos traduzidos é proibido.

Construir um bot de arbitragem: Encontrar 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 esta, está ngmi

Este artigo é 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 bolsas 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. Por fim, 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, temos de definir claramente o perímetro do nosso bot de arbitragem. Especificamente, que tipo de arbitragens queremos realizar. O tipo de arbitragem mais seguro é entre pools que envolvem ETH. Uma vez que o ETH é o ativo com o qual o gás das nossas transações é pago, é natural querer sempre acabar com ETH após uma arbitragem. Mas todos são tentados a pensar assim. Tenha em mente que, no trading, oportunidades pontuais se tornam menos e menos lucrativas à medida que mais pessoas atuam sobre elas.

Para simplificar, vamos focar nas oportunidades de arbitragem entre pools que envolvem ETH. Apenas procuraremos oportunidades entre duas 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 multi-hop). Note que aprimorar esta estratégia para uma mais arriscada é o primeiro passo que deve dar para melhorar a rentabilidade do seu robô.

Para melhorar esta estratégia, poderia, por exemplo, manter algum inventário em stablecoins e atuar em oportunidades de arbitragem que rendam stablecoins. O mesmo poderia ser feito para ativos muito mais arriscados como shitcoins (com as precauções necessárias) e reequilibrar periodicamente a sua carteira em ETH para pagar o gás.

Outra direção seria abandonar a suposição implícita de atomicidade que fizemos e introduzir o 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 exchanges centralizadas muito mais eficientes, ou aquelas que estão, mas cujo preço não é rastreado corretamente na cadeia. Isso envolve muitas partes móveis e está fora do escopo desta série.

Seleção dos pares de tokens

Agora que definimos o perímetro do nosso bot 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 implementados 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)# [...]# Obter lista de pools para cada contrato de fábricapairDataList = []for factoryName, factoryData in fábricas.items():eventos = getPairEvents(w3.eth.contract(address=factoryData['factory'], abi=factory_abi), 0, w3.eth.block_number)print(f'Encontradas {len(eventos)} pools para {factoryName}')for e in eventos:   pairDataList.append({       "token0": e["args"]["token0"],       "token1": e["args"]["token1"],       "pair": e["args"]["pair"],       "fábrica": factoryName   })

Vamos simplesmente inverter pairDataList num 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 selecionadas serão armazenados em listas com pelo menos 2 elementos:

# [...]WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"pair_pool_dict = {}for pair_object in pairDataList:# Verifique se o ETH (WETH) está no par.pair = (pair_object['token0'], pair_object['token1'])if WETH not in pair:   continue# Certifique-se de que o par está referenciado no dicionário. if pair not in pair_pool_dict:   pair_pool_dict[pair] = []# Adicione o pool à lista de pools que negociam este par.pair_pool_dict[pair].append(pair_object)# Crie o dicionário final de pools que serão negociadas.pool_dict = {}for pair, pool_list in pair_pool_dict.items():if len(pool_list) >= 2:   pool_dict[pair] = pool_list

Alguns dados devem ser impressos para termos uma melhor compreensão dos dados com os quais estamos a trabalhar:

# 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, décimaspool_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 décimos: {pool_count_list[::int(len(pool_count_list)/10)]}')# Distribuição do número de pools por par, percentis (décimos do primeiro décimo)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]}')

Neste momento, isto gera o seguinte:

Temos 1431 pares diferentes.

Temos um total de 3081 pools.

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

Número de pools por par, em decilas: [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]

A obtenção de reservas para 3000 pools pode ser feita em menos de 1 segundo com nós RPC públicos. Este é um período de tempo razoável.

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

Encontrar oportunidades de arbitragem

Idea geral

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

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 irá descer, e o preço da pool que vende irá subir. O movimento dos preços é descrito pela fórmula do produto constante.

Já vimos em @emileamajar/construir-um-bot-de-arbitragem-criadores-de-mercado-automatizados-e-uniswap-2d208215d8c2">artigo 1 como calcular a saída de uma troca através de uma pool, dado os reservatórios dessa pool e a quantidade de entrada.

Para encontrar o tamanho de negociação ideal, primeiro encontramos uma fórmula para a saída de duas trocas sucessivas, dado um determinado montante de entrada e as reservas dos dois pools envolvidos nas trocas.

Assumimos que a entrada da primeira troca é em token0 e a entrada da segunda troca é em token1, o que finalmente resulta numa saída em token0.

Deixe x ser a quantidade de entrada, (a1, b1) as reservas da primeira pool e (a2, b2) as reservas da segunda pool. A taxa é a taxa cobrada pelas pools e assume-se que é a mesma para ambas as pools (na maior parte do tempo, 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-taxa)))

O resultado 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-taxa)))

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

Podemos plotar esta função usando desmosAo escolher os valores de reserva de forma a simular que o primeiro pool tem 1 ETH e 1750 USDC, e o segundo pool tem 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

Note que na verdade traçamos out2(x) - x, que é o lucro do negócio, menos o montante de entrada.

Graficamente, podemos ver que o tamanho de negociação ótimo é 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 explorar esta 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 conta o custo do gás da transação. Isso será discutido num artigo seguinte.

Queremos calcular automaticamente este tamanho de negociação ótimo para o nosso bot de MEV. Isto pode ser feito através de cálculo elemental. Temos uma função de uma variável x que queremos maximizar. A função atinge o seu máximo para um valor de x onde a derivada da função é 0.

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

Encontrar a derivada da 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-fee)^4 (b1(1-taxa)+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 (comparado 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 o resultado das duas trocas e o tamanho ótimo da negociação:

# Funções auxiliares para calcular o tamanho de negociação ideal# Saída de uma única troca de swapdef swap_output(x, a, b, taxa=0.003):return b * (1 - a/(a + x*(1-taxa)))# Lucro bruto de duas trocas sucessivasdef trade_profit(x, reservas1, reservas2, taxa=0.003): a1, b1 = reservas1a2, b2 = reservas2return swap_output(swap_output(x, a1, b1, taxa), b2, a2, taxa) - x# Quantidade de entrada idealdef optimal_trade_size(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 de 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 da nossa estratégia.

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

Primeiro faremos uma estimativa bruta do custo de gás, assumindo um custo fixo de transação de gás (um limite inferior, na verdade), e eliminaremos as oportunidades que não são suficientemente lucrativas para cobrir o custo de gás. Só então faremos uma estimativa precisa do custo de 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 in pool_list: to_fetch.append(pair_object["pair"]) # Adicione o endereço do poolprint(f"Buscando reservas de {len(to_fetch)} pools...")# getReservesParallel() é do artigo 2 da série de bots MEVRESERVEList = 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 posterior para pair_object em pool_list: pair_object["reserves"] = reserveList[index] index += 1# Itere sobre todos os pools do parefor poolA em pool_list: para poolB em pool_list: # Pule 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["reserves"] ou 0 em poolB["reserves"]: continue # Reordene as reservas para que WETH seja sempre o primeiro token se poolA["token0"] == WETH: res_A = (poolA["reserves"][0], poolA["reserves"][1]) res_B = (poolB["reserves"][0], poolB["reserves"][1]) else: res_A = (poolA["reserves"][1],  poolA["reserves"][0]) res_B = (poolB["reserves"][1], poolB["reserves"][0]) # Calcule o valor da entrada ótima através da fórmula x = optimal_trade_size(res_A, res_B) # Pule se a entrada ótima for negativa (a ordem dos pools é invertida) se x < 0: continue # Calcule o lucro bruto em Wei (antes do custo do gás) lucro = trade_profit(x,  res_A, res_B) # Armazene os detalhes da oportunidade. Os valores estão em ETH. (1e18 Wei = 1 ETH) opps.append({ "lucro": lucro / 1e18, "entrada": x / 1e18, "par": par, "poolA": poolA, "poolB": poolB, })print(f"Found {len(opps)} opportunities.")

Que produz a seguinte saída:

Obtendo reservas de 3081 pools.

Encontradas 1791 oportunidades.

Agora temos uma lista de todas as oportunidades. Apenas precisamos estimar o lucro delas. Neste momento, simplesmente assumiremos um custo de gás constante para negociar 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á próximo de 43k de gás.

Aproveitar uma oportunidade requer 2 swaps e a execução de uma transação na Ethereum custa um valor fixo de 21k de gás, num total de 107k de gás por oportunidade.

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

# [...]# Utilize o custo de gás codificado de 107k gás por oportunidade gp = w3.eth.gas_pricefor opp in 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 positivasa_imprimir(f"Encontradas {len(positive_opps)} oportunidades positivas.")# Detalhes sobre cada oportunidade ETH_PRICE = 1900 # Você deve buscar dinamicamente o preço do ETHpara opp em positive_opps:a_imprimir(f"Lucro: {opp['net_profit']} ETH (${opp['net_profit'] * ETH_PRICE})")a_imprimir(f"Entrada: {opp['input']} ETH (${opp['input'] * ETH_PRICE})")a_imprimir(f"Pool A: {opp['poolA']['pair']}")a_imprimir(f"Pool B: {opp['poolB']['pair']}")a_imprimir()

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)

Input: 0.6715171730669338 ETH ($1275.8826288271744)

Pool A: 0x1f1B4836Dde1859e2edE1C6155140318EF5931C2

Pool B: 0x1f7efDcD748F43Fc4BeAe6897e5a6DDd865DcceA

{'profit': 0.8161587894746954, 'input': 0.671(…)

(...)

Que lucros estranhamente altos. O primeiro passo a ser dado é verificar se o código está correto. Depois de verificar cuidadosamente o código, descobrimos que o código está correto.

Estes lucros são reais? Acontece que não. Lançamos a nossa rede demasiado ampla ao selecionar quais pools considerar na nossa estratégia e acabámos por obter pools de tokens tóxicos.

O padrão de token ERC20 apenas descreve uma interface para interoperabilidade. Qualquer pessoa pode implementar um token que segue esta interface e optar por implementar comportamentos não ortodoxos, que é exatamente o que está em jogo aqui.

Alguns criadores de tokens criam os seus ERC20 de forma a que as pools em que são negociados não possam vender, mas apenas comprar o token. Alguns contratos de tokens até têm mecanismos de botão de desligar que permitem ao criador retirar todos os utilizadores.

No nosso bot de MEV, estes tokens tóxicos devem ser filtrados. Isso será abordado num artigo futuro.

Se filtrarmos manualmente os tokens obviamente tóxicos, ficamos com 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)

Input: 0.014696360216108083 ETH ($27.92308441060536)

Pool A: 0xfDBFb4239935A15C2C348400570E34De3b044c5F

Pool B: 0x0F15d69a7E5998252ccC39Ad239Cef67fa2a9369

{‘profit’: 0.005531063584733992, (…)

Lucro: 0.003693235163284344 ETH ($7.017146810240254)

Input: 0.1392339178514088 ETH ($264.5444439176767)

Pool A: 0x2957215d0473d2c811A075725Da3C31D2af075F1

Pool B: 0xF110783EbD020DCFBA91Cd1976b79a6E510846AA

{‘profit’: 0.005131718332543344, (…)

Lucro: 0.003674128918827048 ETH ($6.980844945771391)

Input: 0.2719041848570484 ETH ($516.617951228392)

Pool A: 0xBa19343ff3E9f496F17C7333cdeeD212D65A8425

Pool B: 0xD30567f1d084f411572f202ebb13261CE9F46325

{‘profit’: 0.005112612088086048, (…)

(...)

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

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

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

Para simular a execução, primeiro precisamos desenvolver o contrato inteligente que irá 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 potenciais oportunidades de arbitragem e precisamos simular a sua execução para obter um valor final de lucro. Para isso, precisamos ter o nosso contrato inteligente de negociação pronto.

No próximo artigo, iremos desenvolver um contrato inteligente em Solidity e simular o nosso primeiro comércio de arbitragem.

Pode encontrar o código completo no repositório do github associado a este artigoO script é melhor executado num notebook Jupyter.

Aviso Legal:

  1. Este artigo é reproduzido a partir de [ médio], Todos os direitos de autor pertencem ao autor original [Emile Amajar]. Título original do artigo "Construir um bot de arbitragem: Encontrar oportunidades de arbitragem (artigo 3/n)", Se houver objeções a esta reimpressão, por favor entre em contato com o Gate Learnequipa, e eles tratarão dela prontamente.
  2. Aviso de responsabilidade: As 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. Salvo indicação em contrário, copiar, distribuir ou plagiar os artigos traduzidos é proibido.
即刻开始交易
注册并交易即可获得
$100
和价值
$5500
理财体验金奖励!