Crie seu próprio agente de negociação de IA no Ethereum
Neste tutorial, você aprenderá a criar um agente simples de negociação de IA. Este agente funciona seguindo estas etapas:
- Ler os preços atuais e passados de um token, bem como outras informações potencialmente relevantes
- Criar uma consulta com essas informações, juntamente com informações de fundo para explicar como elas podem ser relevantes
- Enviar a consulta e receber de volta um preço projetado
- Negociar com base na recomendação
- Aguardar e repetir
Este agente demonstra como ler informações, traduzi-las em uma consulta que produz uma resposta utilizável e usar essa resposta. Todas essas são etapas necessárias para um agente de IA. Este agente é implementado em Python porque é a linguagem mais comum usada em IA.
Por que fazer isso?
Agentes de negociação automatizados permitem que os desenvolvedores selecionem e executem uma estratégia de negociação. agentes de IA permitem estratégias de negociação mais complexas e dinâmicas, usando potencialmente informações e algoritmos que o desenvolvedor nem considerou usar.
As ferramentas
Este tutorial usa Python (opens in a new tab), a biblioteca Web3 (opens in a new tab) e o Uniswap v3 (opens in a new tab) para cotações e negociações.
Por que Python?
A linguagem mais utilizada para IA é o Python (opens in a new tab), por isso a usamos aqui. Não se preocupe se você não conhece Python. A linguagem é muito clara e explicarei exatamente o que ela faz.
A biblioteca Web3 (opens in a new tab) é a API Python Ethereum mais comum. É bem fácil de usar.
Negociando na cadeia de blocos
Existem muitas corretoras distribuídas (DEX) que permitem negociar tokens no Ethereum. No entanto, elas tendem a ter taxas de câmbio semelhantes devido à arbitragem.
O Uniswap (opens in a new tab) é uma DEX amplamente utilizada que podemos usar tanto para cotações (para ver os valores relativos dos tokens) quanto para negociações.
OpenAI
Para um modelo de linguagem grande, escolhi começar com o OpenAI (opens in a new tab). Para executar a aplicação neste tutorial, você precisará pagar pelo acesso à API. O pagamento mínimo de US$ 5 é mais do que suficiente.
Desenvolvimento, passo a passo
Para simplificar o desenvolvimento, prosseguimos em etapas. Cada etapa é um branch no GitHub.
Primeiros passos
Existem etapas para começar no UNIX ou Linux (incluindo o WSL (opens in a new tab))
-
Se ainda não tiver, baixe e instale o Python (opens in a new tab).
-
Clone o repositório do GitHub.
1git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started2cd 260215-ai-agent -
Instale o
uv(opens in a new tab). O comando em seu sistema pode ser diferente.1pipx install uv -
Baixe as bibliotecas.
1uv sync -
Ative o ambiente virtual.
1source .venv/bin/activate -
Para verificar se o Python e o Web3 estão funcionando corretamente, execute o
python3e forneça a ele este programa. Você pode inseri-lo no prompt>>>; não é necessário criar um arquivo.1from web3 import Web32MAINNET_URL = "https://eth.drpc.org"3w3 = Web3(Web3.HTTPProvider(MAINNET_URL))4w3.eth.block_number5quit()
Lendo da cadeia de blocos
O próximo passo é ler da cadeia de blocos. Para fazer isso, você precisa mudar para o branch 02-read-quote e usar o uv para executar o programa.
1git checkout 02-read-quote2uv run agent.pyVocê deve receber uma lista de objetos Quote, cada um com um timestamp, um preço e o ativo (atualmente sempre WETH/USDC).
Aqui está uma explicação linha por linha.
1from web3 import Web32from web3.contract import Contract3from decimal import Decimal, ROUND_HALF_UP4from dataclasses import dataclass5from datetime import datetime, timezone6from pprint import pprint7import time8import functools9import sysExibir tudoImporte as bibliotecas que precisamos. Eles são explicados abaixo quando usados.
1print = functools.partial(print, flush=True)Substitui o print do Python por uma versão que sempre descarrega a saída imediatamente. Isso é útil em um script de longa duração, porque não queremos esperar por atualizações de status ou saídas de depuração.
1MAINNET_URL = "https://eth.drpc.org"Uma URL para chegar à rede principal. Você pode obter uma de um Nó como serviço ou usar uma das anunciadas na Chainlist (opens in a new tab).
1BLOCK_TIME_SECONDS = 122MINUTE_BLOCKS = int(60 / BLOCK_TIME_SECONDS)3HOUR_BLOCKS = MINUTE_BLOCKS * 604DAY_BLOCKS = HOUR_BLOCKS * 24Um bloco da rede principal do Ethereum geralmente acontece a cada doze segundos, então este é o número de blocos que esperamos que aconteçam em um período de tempo. Observe que este não é um número exato. Quando o propositor de bloco está inativo, esse bloco é pulado e o tempo para o próximo bloco é de 24 segundos. Se quiséssemos obter o bloco exato para um timestamp, usaríamos a busca binária (opens in a new tab). No entanto, isso é próximo o suficiente para nossos propósitos. Prever o futuro não é uma ciência exata.
1CYCLE_BLOCKS = DAY_BLOCKSO tamanho do ciclo. Revisamos as cotações uma vez por ciclo e tentamos estimar o valor no final do próximo ciclo.
1# O endereço do pool que estamos lendo2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")Os valores da cotação são retirados do pool USDC/WETH do Uniswap 3 no endereço 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 (opens in a new tab). Este endereço já está no formato checksum, mas é melhor usar Web3.to_checksum_address (opens in a new tab) para tornar o código reutilizável.
1POOL_ABI = [2 { "name": "slot0", ... },3 { "name": "token0", ... },4 { "name": "token1", ... },5]67ERC20_ABI = [8 { "name": "symbol", ... },9 { "name": "decimals", ... }10]Exibir tudoEstas são as IABs (opens in a new tab) para os dois contratos que precisamos contatar. Para manter o código conciso, incluímos apenas as funções que precisamos chamar.
1w3 = Web3(Web3.HTTPProvider(MAINNET_URL))Inicie a biblioteca Web3 (opens in a new tab) e conecte-se a um nó Ethereum.
1@dataclass(frozen=True)2class ERC20Token:3 address: str4 symbol: str5 decimals: int6 contract: ContractEsta é uma maneira de criar uma classe de dados em Python. O tipo de dados Contract (opens in a new tab) é usado para se conectar ao contrato. Observe o (frozen=True). Em Python, os booleanos (opens in a new tab) são definidos como True ou False, com letra maiúscula. Esta classe de dados é frozen (congelada), o que significa que os campos não podem ser modificados.
Observe a indentação. Em contraste com as linguagens derivadas de C (opens in a new tab), o Python usa indentação para denotar blocos. O interpretador Python sabe que a definição a seguir não faz parte desta classe de dados porque não começa na mesma indentação que os campos da classe de dados.
1@dataclass(frozen=True)2class PoolInfo:3 address: str4 token0: ERC20Token5 token1: ERC20Token6 contract: Contract7 asset: str8 decimal_factor: Decimal = 1O tipo Decimal (opens in a new tab) é usado para lidar com precisão com frações decimais.
1 def get_price(self, block: int) -> Decimal:Esta é a maneira de definir uma função em Python. A definição é indentada para mostrar que ainda faz parte de PoolInfo.
Em uma função que faz parte de uma classe de dados, o primeiro parâmetro é sempre self, a instância da classe de dados que a chamou. Aqui há outro parâmetro, o número do bloco.
1 assert block <= w3.eth.block_number, "O bloco está no futuro"Se pudéssemos ler o futuro, não precisaríamos de IA para negociação.
1 sqrt_price_x96 = Decimal(self.contract.functions.slot0().call(block_identifier=block)[0])A sintaxe para chamar uma função na EVM a partir do Web3 é esta: <objeto de contrato>.functions.<nome da função>().call(<parâmetros>). Os parâmetros podem ser os parâmetros da função da EVM (se houver; aqui não há) ou parâmetros nomeados (opens in a new tab) para modificar o comportamento da cadeia de blocos. Aqui usamos um, block_identifier, para especificar o número do bloco no qual desejamos executar.
O resultado é esta struct, em formato de array (opens in a new tab). O primeiro valor é uma função da taxa de câmbio entre os dois tokens.
1 raw_price = (sqrt_price_x96 / Decimal(2**96)) ** 2Para reduzir os cálculos na cadeia, o Uniswap v3 não armazena o fator de câmbio real, mas sim sua raiz quadrada. Como a EVM não suporta matemática de ponto flutuante ou frações, em vez do valor real, a resposta é
1 # (token1 por token0)2 return 1/(raw_price * self.decimal_factor)O preço bruto que obtemos é o número de token0 que obtemos para cada token1. Em nosso pool, o token0 é USDC (moeda estável com o mesmo valor de um dólar americano) e o token1 é WETH (opens in a new tab). O valor que realmente queremos é o número de dólares por WETH, não o inverso.
O fator decimal é a proporção entre os fatores decimais (opens in a new tab) para os dois tokens.
1@dataclass(frozen=True)2class Quote:3 timestamp: str4 price: Decimal5 asset: strEssa classe de dados representa uma cotação: o preço de um ativo específico em um determinado momento. Neste ponto, o campo asset é irrelevante, porque usamos um único pool e, portanto, temos um único ativo. No entanto, adicionaremos mais ativos posteriormente.
1def read_token(address: str) -> ERC20Token:2 token = w3.eth.contract(address=address, abi=ERC20_ABI)3 symbol = token.functions.symbol().call()4 decimals = token.functions.decimals().call()56 return ERC20Token(7 address=address,8 symbol=symbol,9 decimals=decimals,10 contract=token11 )Exibir tudoEsta função recebe um endereço e retorna informações sobre o contrato do token nesse endereço. Para criar um novo Contrato Web3 (opens in a new tab), fornecemos o endereço e a IAB para w3.eth.contract.
1def read_pool(address: str) -> PoolInfo:2 pool_contract = w3.eth.contract(address=address, abi=POOL_ABI)3 token0Address = pool_contract.functions.token0().call()4 token1Address = pool_contract.functions.token1().call()5 token0 = read_token(token0Address)6 token1 = read_token(token1Address)78 return PoolInfo(9 address=address,10 asset=f"{token1.symbol}/{token0.symbol}",11 token0=token0,12 token1=token1,13 contract=pool_contract,14 decimal_factor=Decimal(10) ** Decimal(token0.decimals - token1.decimals)15 )Exibir tudoEsta função retorna tudo o que precisamos sobre um pool específico (opens in a new tab). A sintaxe f"<string>" é uma string formatada (opens in a new tab).
1def get_quote(pool: PoolInfo, block_number: int = None) -> Quote:Obtenha um objeto Quote. O valor padrão para block_number é None (sem valor).
1 if block_number is None:2 block_number = w3.eth.block_numberSe um número de bloco não foi especificado, use w3.eth.block_number, que é o número do bloco mais recente. Essa é a sintaxe para uma instrução if (opens in a new tab).
Pode parecer que seria melhor apenas definir o padrão como w3.eth.block_number, mas isso não funciona bem porque seria o número do bloco no momento em que a função é definida. Em um agente de longa duração, isso seria um problema.
1 block = w3.eth.get_block(block_number)2 price = pool.get_price(block_number)3 return Quote(4 timestamp=datetime.fromtimestamp(block.timestamp, timezone.utc).isoformat(),5 price=price.quantize(Decimal("0.01")),6 asset=pool.asset7 )Use a biblioteca datetime (opens in a new tab) para formatá-lo em um formato legível para humanos e modelos de linguagem grandes (LLMs). Use Decimal.quantize (opens in a new tab) para arredondar o valor para duas casas decimais.
1def get_quotes(pool: PoolInfo, start_block: int, end_block: int, step: int) -> list[Quote]:Em Python, você define uma lista (opens in a new tab) que só pode conter um tipo específico usando list[<type>].
1 quotes = []2 for block in range(start_block, end_block + 1, step):Em Python, um loop for (opens in a new tab) normalmente itera sobre uma lista. A lista de números de bloco para encontrar cotações vem de range (opens in a new tab).
1 quote = get_quote(pool, block)2 quotes.append(quote)3 return quotesPara cada número de bloco, obtenha um objeto Quote e anexe-o à lista quotes. Em seguida, retorne essa lista.
1pool = read_pool(WETHUSDC_ADDRESS)2quotes = get_quotes(3 pool,4 w3.eth.block_number - 12*CYCLE_BLOCKS,5 w3.eth.block_number,6 CYCLE_BLOCKS7)89pprint(quotes)Exibir tudoEste é o código principal do script. Leia as informações do pool, obtenha doze cotações e use pprint (opens in a new tab) para exibi-las.
Criando um prompt
Em seguida, precisamos converter essa lista de cotações em um prompt para um LLM e obter um valor futuro esperado.
1git checkout 03-create-prompt2uv run agent.pyA saída agora será um prompt para um LLM, semelhante a:
1Dadas estas cotações:2Ativo: WETH/USDC3 2026-01-20T16:34 3016.214 .5 .6 .7 2026-02-01T17:49 2299.1089Ativo: WBTC/WETH10 2026-01-20T16:34 29.8411 .12 .13 .14 2026-02-01T17:50 33.46151617Qual você esperaria que fosse o valor para WETH/USDC no momento 2026-02-02T17:56?1819Forneça sua resposta como um único número arredondado para duas casas decimais,20sem nenhum outro texto.Exibir tudoObserve que há cotações para dois ativos aqui, WETH/USDC e WBTC/WETH. Adicionar cotações de outro ativo pode melhorar a precisão da previsão.
Como é um prompt
Este prompt contém três seções, que são bastante comuns em prompts de LLM.
-
Informação. Os LLMs têm muitas informações de seu treinamento, mas geralmente não têm as mais recentes. É por isso que precisamos recuperar as cotações mais recentes aqui. Adicionar informações a um prompt é chamado de geração aumentada por recuperação (RAG) (opens in a new tab).
-
A pergunta real. É isso que queremos saber.
-
Instruções de formatação de saída. Normalmente, um LLM nos dará uma estimativa com uma explicação de como chegou a ela. Isso é melhor para humanos, mas um programa de computador só precisa do resultado final.
Explicação do código
Aqui está o novo código.
1from datetime import datetime, timezone, timedeltaPrecisamos fornecer ao LLM o tempo para o qual queremos uma estimativa. Para obter um tempo "n minutos/horas/dias" no futuro, usamos a classe timedelta (opens in a new tab).
1# Os endereços dos pools que estamos lendo2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")3WETHWBTC_ADDRESS = Web3.to_checksum_address("0xCBCdF9626bC03E24f779434178A73a0B4bad62eD")Temos dois pools que precisamos ler.
1@dataclass(frozen=True)2class PoolInfo:3 .4 .5 .6 reverse: bool = False78 def get_price(self, block: int) -> Decimal:9 assert block <= w3.eth.block_number, "Block is in the future"10 sqrt_price_x96 = Decimal(self.contract.functions.slot0().call(block_identifier=block)[0])11 raw_price = (sqrt_price_x96 / Decimal(2**96)) ** 2 # (token1 per token0)12 if self.reverse:13 return 1/(raw_price * self.decimal_factor)14 else:15 return raw_price * self.decimal_factorExibir tudoNo pool WETH/USDC, queremos saber quantos token0 (USDC) precisamos para comprar um token1 (WETH). No pool WETH/WBTC, queremos saber quantos token1 (WETH) precisamos para comprar um token0 (WBTC, que é Bitcoin embrulhado). Precisamos rastrear se a proporção do pool precisa ser invertida.
1def read_pool(address: str, reverse: bool = False) -> PoolInfo:2 .3 .4 .56 return PoolInfo(7 .8 .9 .1011 asset= f"{token1.symbol}/{token0.symbol}" if reverse else f"{token0.symbol}/{token1.symbol}",12 reverse=reverse13 )Exibir tudoPara saber se um pool precisa ser revertido, precisamos obter isso como entrada para read_pool. Além disso, o símbolo do ativo precisa ser configurado corretamente.
A sintaxe <a> if <b> else <c> é o equivalente em Python do operador condicional ternário (opens in a new tab), que em uma linguagem derivada de C seria <b> ? <a> : <c>.
1def format_quotes(quotes: list[Quote]) -> str:2 result = f"Asset: {quotes[0].asset}\n"3 for quote in quotes:4 result += f"\t{quote.timestamp[0:16]} {quote.price.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)}\n"5 return resultEsta função cria uma string que formata uma lista de objetos Quote, supondo que todos se apliquem ao mesmo ativo.
1def make_prompt(quotes: list[list[Quote]], expected_time: str, asset: str) -> str:2 return f"""Em Python, os literais de string de várias linhas (opens in a new tab) são escritos como """ .... """.
1Dadas estas cotações:2{3 functools.reduce(lambda acc, q: acc + '\n' + q,4 map(lambda q: format_quotes(q), quotes))5}Aqui, usamos o padrão MapReduce (opens in a new tab) para gerar uma string para cada lista de cotações com format_quotes e, em seguida, reduzi-las a uma única string para uso no prompt.
1Qual você esperaria que fosse o valor para {asset} no momento {expected_time}?23Forneça sua resposta como um único número arredondado para duas casas decimais,4sem nenhum outro texto.5 """O resto do prompt é como esperado.
1wethusdc_pool = read_pool(WETHUSDC_ADDRESS, True)2wethusdc_quotes = get_quotes(3 wethusdc_pool,4 w3.eth.block_number - 12*CYCLE_BLOCKS,5 w3.eth.block_number,6 CYCLE_BLOCKS,7)89wethwbtc_pool = read_pool(WETHWBTC_ADDRESS)10wethwbtc_quotes = get_quotes(11 wethwbtc_pool,12 w3.eth.block_number - 12*CYCLE_BLOCKS,13 w3.eth.block_number,14 CYCLE_BLOCKS15)Exibir tudoRevise os dois pools e obtenha cotações de ambos.
1future_time = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat()[0:16]23print(make_prompt(wethusdc_quotes + wethwbtc_quotes, future_time, wethusdc_pool.asset))Determine o ponto de tempo futuro para o qual queremos a estimativa e crie o prompt.
Interface com um LLM
Em seguida, solicitamos um LLM real e recebemos um valor futuro esperado. Eu escrevi este programa usando OpenAI, então se você quiser usar um provedor diferente, precisará ajustá-lo.
-
Obtenha uma conta OpenAI (opens in a new tab)
-
Financie a conta (opens in a new tab) — o valor mínimo no momento da escrita é de US$ 5
-
Na linha de comando, exporte a chave de API para que seu programa possa usá-la
1export OPENAI_API_KEY=sk-<o restante da chave vai aqui> -
Faça o checkout e execute o agente
1git checkout 04-interface-llm2uv run agent.py
Aqui está o novo código.
1from openai import OpenAI23open_ai = OpenAI() # O cliente lê a variável de ambiente OPENAI_API_KEYImporte e instancie a API OpenAI.
1response = open_ai.chat.completions.create(2 model="gpt-4-turbo",3 messages=[4 {"role": "user", "content": prompt}5 ],6 temperature=0.0,7 max_tokens=16,8)Chame a API OpenAI (open_ai.chat.completions.create) para criar a resposta.
1expected_price = Decimal(response.choices[0].message.content.strip())2current_price = wethusdc_quotes[-1].price34print ("Current price:", wethusdc_quotes[-1].price)5print(f"In {future_time}, expected price: {expected_price} USD")67if (expected_price > current_price):8 print(f"Buy, I expect the price to go up by {expected_price - current_price} USD")9else:10 print(f"Sell, I expect the price to go down by {current_price - expected_price} USD")Exibir tudoMostre o preço e forneça uma recomendação de compra ou venda.
Testando as previsões
Agora que podemos gerar previsões, também podemos usar dados históricos para avaliar se produzimos previsões úteis.
1uv run test-predictor.pyO resultado esperado é semelhante a:
1Previsão para 2026-01-05T19:50: previsto 3138.93 USD, real 3218.92 USD, erro 79.99 USD2Previsão para 2026-01-06T19:56: previsto 3243.39 USD, real 3221.08 USD, erro 22.31 USD3Previsão para 2026-01-07T20:02: previsto 3223.24 USD, real 3146.89 USD, erro 76.35 USD4Previsão para 2026-01-08T20:11: previsto 3150.47 USD, real 3092.04 USD, erro 58.43 USD5.6.7.8Previsão para 2026-01-31T22:33: previsto 2637.73 USD, real 2417.77 USD, erro 219.96 USD9Previsão para 2026-02-01T22:41: previsto 2381.70 USD, real 2318.84 USD, erro 62.86 USD10Previsão para 2026-02-02T22:49: previsto 2234.91 USD, real 2349.28 USD, erro 114.37 USD11Erro médio de previsão em 29 previsões: 83.87103448275862068965517241 USD12Mudança média por recomendação: 4.787931034482758620689655172 USD13Variância padrão das mudanças: 104.42 USD14Dias lucrativos: 51.72%15Dias com perdas: 48.28%Exibir tudoA maior parte do testador é idêntica ao agente, mas aqui estão as partes que são novas ou modificadas.
1CYCLES_FOR_TEST = 40 # Para o backtest, quantos ciclos testamos23# Obtenha muitas cotações4wethusdc_pool = read_pool(WETHUSDC_ADDRESS, True)5wethusdc_quotes = get_quotes(6 wethusdc_pool,7 w3.eth.block_number - CYCLE_BLOCKS*CYCLES_FOR_TEST,8 w3.eth.block_number,9 CYCLE_BLOCKS,10)1112wethwbtc_pool = read_pool(WETHWBTC_ADDRESS)13wethwbtc_quotes = get_quotes(14 wethwbtc_pool,15 w3.eth.block_number - CYCLE_BLOCKS*CYCLES_FOR_TEST,16 w3.eth.block_number,17 CYCLE_BLOCKS18)Exibir tudoAnalisamos CYCLES_FOR_TEST (especificado como 40 aqui) dias para trás.
1# Crie previsões e verifique-as em relação ao histórico real23total_error = Decimal(0)4changes = []Existem dois tipos de erros nos quais estamos interessados. O primeiro, total_error, é simplesmente a soma dos erros que o previsor cometeu.
Para entender o segundo, changes, precisamos nos lembrar do propósito do agente. Não é prever a proporção WETH/USDC (preço do ETH). É para emitir recomendações de venda e compra. Se o preço atual for US$ 2.000 e ele prever US$ 2.010 para amanhã, não nos importamos se o resultado real for US$ 2.020 e ganharmos dinheiro extra. Mas nós nos importamos se ele previu US$ 2.010 e comprou ETH com base nessa recomendação, e o preço cair para US$ 1.990.
1for index in range(0,len(wethusdc_quotes)-CYCLES_BACK):Só podemos analisar os casos em que o histórico completo (os valores usados para a previsão e o valor do mundo real para compará-lo) está disponível. Isso significa que o caso mais recente deve ser aquele que começou há CYCLES_BACK.
1 wethusdc_slice = wethusdc_quotes[index:index+CYCLES_BACK]2 wethwbtc_slice = wethwbtc_quotes[index:index+CYCLES_BACK]Use fatias (slices) (opens in a new tab) para obter o mesmo número de amostras que o número que o agente usa. O código entre aqui e o próximo segmento é o mesmo código para obter uma previsão que temos no agente.
1 predicted_price = Decimal(response.choices[0].message.content.strip())2 real_price = wethusdc_quotes[index+CYCLES_BACK].price3 prediction_time_price = wethusdc_quotes[index+CYCLES_BACK-1].priceObtenha o preço previsto, o preço real e o preço no momento da previsão. Precisamos do preço no momento da previsão para determinar se a recomendação foi de compra ou de venda.
1 error = abs(predicted_price - real_price)2 total_error += error3 print (f"Prediction for {prediction_time}: predicted {predicted_price} USD, real {real_price} USD, error {error} USD")Calcule o erro e adicione-o ao total.
1 recomended_action = 'buy' if predicted_price > prediction_time_price else 'sell'2 price_increase = real_price - prediction_time_price3 changes.append(price_increase if recomended_action == 'buy' else -price_increase)Para changes, queremos o impacto monetário da compra ou venda de um ETH. Então, primeiro, precisamos determinar a recomendação, depois avaliar como o preço real mudou e se a recomendação gerou lucro (mudança positiva) ou prejuízo (mudança negativa).
1print (f"Mean prediction error over {len(wethusdc_quotes)-CYCLES_BACK} predictions: {total_error / Decimal(len(wethusdc_quotes)-CYCLES_BACK)} USD")23length_changes = Decimal(len(changes))4mean_change = sum(changes, Decimal(0)) / length_changes5print (f"Mean change per recommendation: {mean_change} USD")6var = sum((x - mean_change) ** 2 for x in changes) / length_changes7print (f"Standard variance of changes: {var.sqrt().quantize(Decimal("0.01"))} USD")Relate os resultados.
1print (f"Profitable days: {len(list(filter(lambda x: x > 0, changes)))/length_changes:.2%}")2print (f"Losing days: {len(list(filter(lambda x: x < 0, changes)))/length_changes:.2%}")Use o filter (opens in a new tab) para contar o número de dias lucrativos e o número de dias com prejuízo. O resultado é um objeto de filtro, que precisamos converter em uma lista para obter o comprimento.
Envio de transações
Agora precisamos realmente enviar as transações. No entanto, não quero gastar dinheiro de verdade neste momento, antes que o sistema seja comprovado. Em vez disso, criaremos uma bifurcação local da rede principal e "negociaremos" nessa rede.
Aqui estão as etapas para criar uma bifurcação local e habilitar a negociação.
-
Instale o Foundry (opens in a new tab)
-
Inicie o
anvil(opens in a new tab)1anvil --fork-url https://eth.drpc.org --block-time 12anvilestá escutando na URL padrão para o Foundry, http://localhost:8545 (opens in a new tab), então não precisamos especificar a URL para o comandocast(opens in a new tab) que usamos para manipular a cadeia de blocos. -
Ao executar no
anvil, há dez contas de teste que têm ETH — defina as variáveis de ambiente para a primeira1PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff802ADDRESS=`cast wallet address $PRIVATE_KEY` -
Estes são os contratos que precisamos usar. O
SwapRouter(opens in a new tab) é o contrato Uniswap v3 que usamos para negociar de fato. Poderíamos negociar diretamente através do pool, mas isso é muito mais fácil.As duas variáveis inferiores são os caminhos do Uniswap v3 necessários para trocar entre WETH e USDC.
1WETH_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc22USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f56404SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C058615645WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB486USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 -
Cada uma das contas de teste tem 10.000 ETH. Use o contrato WETH para embrulhar 1000 ETH e obter 1000 WETH para negociação.
1cast send $WETH_ADDRESS "deposit()" --value 1000ether --private-key $PRIVATE_KEY -
Use o
SwapRouterpara negociar 500 WETH por USDC.1cast send $WETH_ADDRESS "approve(address,uint256)" $SWAP_ROUTER 500ether --private-key $PRIVATE_KEY2MAXINT=`cast max-int uint256`3cast send $SWAP_ROUTER \4 "exactInput((bytes,address,uint256,uint256,uint256))" \5 "($WETH_TO_USDC,$ADDRESS,$MAXINT,500ether,1000000)" \6 --private-key $PRIVATE_KEYA chamada
approvecria uma permissão que permite que oSwapRoutergaste alguns dos nossos tokens. Contratos não podem monitorar eventos, portanto, se transferirmos tokens diretamente para o contratoSwapRouter, ele não saberia que foi pago. Em vez disso, permitimos que o contratoSwapRoutergaste uma certa quantia, e então oSwapRoutero faz. Isso é feito através de uma função chamada peloSwapRouter, para que ele saiba se foi bem-sucedido. -
Verifique se você tem o suficiente de ambos os tokens.
1cast call $WETH_ADDRESS "balanceOf(address)" $ADDRESS | cast from-wei2echo `cast call $USDC_ADDRESS "balanceOf(address)" $ADDRESS | cast to-dec`/10^6 | bc
Agora que temos WETH e USDC, podemos realmente executar o agente.
1git checkout 05-trade2uv run agent.pyA saída será semelhante a:
1(ai-trading-agent) qbzzt@Ori-Cloudnomics:~/260215-ai-agent$ uv run agent.py2Preço atual: 1843.163Em 2026-02-06T23:07, preço esperado: 1724.41 USD4Saldos da conta antes da negociação:5Saldo de USDC: 927301.5782726Saldo de WETH: 5007Vender, espero que o preço caia 118,75 USD8Transação de aprovação enviada: 74e367ddbb407c1aaf567d87aa5863049991b1d2aa092b6b85195d925e2bd41f9Transação de aprovação minerada.10Transação de venda enviada: fad1bcf938585c9e90364b26ac7a80eea9efd34c37e5db81e58d7655bcae28bf11Transação de venda minerada.12Saldos da conta após a negociação:13Saldo de USDC: 929143.79711614Saldo de WETH: 499Exibir tudoPara realmente usá-lo, você precisa de algumas pequenas alterações.
- Na linha 14, altere
MAINNET_URLpara um ponto de acesso real, comohttps://eth.drpc.org - Na linha 28, altere
PRIVATE_KEYpara sua própria chave privada - A menos que você seja muito rico e possa comprar ou vender 1 ETH por dia para um agente não comprovado, você pode querer alterar a linha 29 para diminuir o
WETH_TRADE_AMOUNT
Explicação do código
Aqui está o novo código.
1SWAP_ROUTER_ADDRESS=Web3.to_checksum_address("0xE592427A0AEce92De3Edee1F18E0157C05861564")2WETH_TO_USDC=bytes.fromhex("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")3USDC_TO_WETH=bytes.fromhex("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")4PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"As mesmas variáveis que usamos na etapa 4.
1WETH_TRADE_AMOUNT=1A quantia a ser negociada.
1ERC20_ABI = [2 { "name": "symbol", ... },3 { "name": "decimals", ... },4 { "name": "balanceOf", ...},5 { "name": "approve", ...}6]Para realmente negociar, precisamos da função approve. Também queremos mostrar os saldos antes e depois, então também precisamos de balanceOf.
1SWAP_ROUTER_ABI = [2 { "name": "exactInput", ...},3]Na IAB do SwapRouter, só precisamos de exactInput. Existe uma função relacionada, exactOutput, que poderíamos usar para comprar exatamente um WETH, mas para simplificar, usamos apenas exactInput em ambos os casos.
1account = w3.eth.account.from_key(PRIVATE_KEY)2swap_router = w3.eth.contract(3 address=SWAP_ROUTER_ADDRESS,4 abi=SWAP_ROUTER_ABI5)As definições do Web3 para a conta (opens in a new tab) e o contrato SwapRouter.
1def txn_params() -> dict:2 return {3 "from": account.address,4 "value": 0,5 "gas": 300000,6 "nonce": w3.eth.get_transaction_count(account.address),7 }Os parâmetros da transação. Precisamos de uma função aqui porque o nonce (opens in a new tab) deve mudar a cada vez.
1def approve_token(contract: Contract, amount: int):Aprove uma permissão de token para o SwapRouter.
1 txn = contract.functions.approve(SWAP_ROUTER_ADDRESS, amount).build_transaction(txn_params())2 signed_txn = w3.eth.account.sign_transaction(txn, private_key=PRIVATE_KEY)3 tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction)É assim que enviamos uma transação no Web3. Primeiro, usamos o objeto Contract (opens in a new tab) para construir a transação. Em seguida, usamos web3.eth.account.sign_transaction (opens in a new tab) para assinar a transação, usando PRIVATE_KEY. Finalmente, usamos w3.eth.send_raw_transaction (opens in a new tab) para enviar a transação.
1 print(f"Approve transaction sent: {tx_hash.hex()}")2 w3.eth.wait_for_transaction_receipt(tx_hash)3 print("Approve transaction mined.")w3.eth.wait_for_transaction_receipt (opens in a new tab) espera até que a transação seja minerada. Ele retorna o recibo, se necessário.
1SELL_PARAMS = {2 "path": WETH_TO_USDC,3 "recipient": account.address,4 "deadline": 2**256 - 1,5 "amountIn": WETH_TRADE_AMOUNT * 10 ** wethusdc_pool.token1.decimals,6 "amountOutMinimum": 0,7}Estes são os parâmetros ao vender WETH.
1def make_buy_params(quote: Quote) -> dict:2 return {3 "path": USDC_TO_WETH,4 "recipient": account.address,5 "deadline": 2**256 - 1,6 "amountIn": int(quote.price*WETH_TRADE_AMOUNT) * 10**wethusdc_pool.token0.decimals,7 "amountOutMinimum": 0,8 }Em contraste com SELL_PARAMS, os parâmetros de compra podem mudar. O valor de entrada é o custo de 1 WETH, conforme disponível em quote.
1def buy(quote: Quote):2 buy_params = make_buy_params(quote)3 approve_token(wethusdc_pool.token0.contract, buy_params["amountIn"])4 txn = swap_router.functions.exactInput(buy_params).build_transaction(txn_params())5 signed_txn = w3.eth.account.sign_transaction(txn, private_key=PRIVATE_KEY)6 tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction)7 print(f"Buy transaction sent: {tx_hash.hex()}")8 w3.eth.wait_for_transaction_receipt(tx_hash)9 print("Buy transaction mined.")101112def sell():13 approve_token(wethusdc_pool.token1.contract,14 WETH_TRADE_AMOUNT * 10**wethusdc_pool.token1.decimals)15 txn = swap_router.functions.exactInput(SELL_PARAMS).build_transaction(txn_params())16 signed_txn = w3.eth.account.sign_transaction(txn, private_key=PRIVATE_KEY)17 tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction)18 print(f"Sell transaction sent: {tx_hash.hex()}")19 w3.eth.wait_for_transaction_receipt(tx_hash)20 print("Sell transaction mined.")Exibir tudoAs funções buy() e sell() são quase idênticas. Primeiro, aprovamos uma permissão suficiente para o SwapRouter e, em seguida, o chamamos com o caminho e a quantia corretos.
1def balances():2 token0_balance = wethusdc_pool.token0.contract.functions.balanceOf(account.address).call()3 token1_balance = wethusdc_pool.token1.contract.functions.balanceOf(account.address).call()45 print(f"{wethusdc_pool.token0.symbol} Balance: {Decimal(token0_balance) / Decimal(10 ** wethusdc_pool.token0.decimals)}")6 print(f"{wethusdc_pool.token1.symbol} Balance: {Decimal(token1_balance) / Decimal(10 ** wethusdc_pool.token1.decimals)}")Relate os saldos do usuário em ambas as moedas.
1print("Saldos da conta antes da negociação:")2balances()34if (expected_price > current_price):5 print(f"Comprar, espero que o preço suba em {expected_price - current_price} USD")6 buy(wethusdc_quotes[-1])7else:8 print(f"Vender, espero que o preço caia em {current_price - expected_price} USD")9 sell()1011print("Saldos da conta após a negociação:")12balances()Exibir tudoEste agente atualmente só funciona uma vez. No entanto, você pode alterá-lo para funcionar continuamente, executando-o a partir do crontab (opens in a new tab) ou envolvendo as linhas 368-400 em um loop e usando time.sleep (opens in a new tab) para esperar até a hora do próximo ciclo.
Possíveis melhorias
Esta não é uma versão de produção completa; é apenas um exemplo para ensinar o básico. Aqui estão algumas ideias de melhorias.
Negociação mais inteligente
Há dois fatos importantes que o agente ignora ao decidir o que fazer.
- A magnitude da mudança antecipada. O agente vende uma quantia fixa de
WETHse o preço for esperado para diminuir, independentemente da magnitude do declínio. Pode-se argumentar que seria melhor ignorar pequenas alterações e vender com base no quanto esperamos que o preço caia. - O portfólio atual. Se 10% do seu portfólio estiver em WETH e você achar que o preço vai subir, provavelmente faz sentido comprar mais. Mas se 90% do seu portfólio estiver em WETH, você pode estar suficientemente exposto e não há necessidade de comprar mais. O inverso é verdadeiro se você espera que o preço caia.
E se você quiser manter sua estratégia de negociação em segredo?
Os fornecedores de IA podem ver as consultas que você envia para seus LLMs, o que poderia expor o sistema de negociação genial que você desenvolveu com seu agente. Um sistema de negociação que muitas pessoas usam não tem valor, porque muitas pessoas tentam comprar quando você quer comprar (e o preço sobe) e tentam vender quando você quer vender (e o preço cai).
Você pode executar um LLM localmente, por exemplo, usando o LM-Studio (opens in a new tab), para evitar este problema.
De bot de IA para agente de IA
Você pode argumentar que este é um bot de IA, não um agente de IA. Ele implementa uma estratégia relativamente simples que depende de informações predefinidas. Podemos habilitar a auto-melhoria, por exemplo, fornecendo uma lista de pools do Uniswap v3 e seus valores mais recentes e perguntando qual combinação tem o melhor valor preditivo.
Proteção contra slippage
Atualmente não há proteção contra slippage (opens in a new tab). Se a cotação atual for de US$ 2.000 e o preço esperado for de US$ 2.100, o agente comprará. No entanto, se antes de o agente comprar o custo subir para US$ 2.200, não faz mais sentido comprar.
Para implementar a proteção contra slippage, especifique um valor de amountOutMinimum nas linhas 325 e 334 de agent.py (opens in a new tab).
Conclusão
Esperamos que agora você saiba o suficiente para começar a usar agentes de IA. Esta não é uma visão geral abrangente do assunto; existem livros inteiros dedicados a isso, mas isso é suficiente para você começar. Boa sorte!
Veja aqui mais do meu trabalho (opens in a new tab).
Última atualização da página: 10 de fevereiro de 2026