Pular para o conteúdo principal

Crie seu próprio agente de negociação de IA no Ethereum

IA
negociação
agente
python
Intermediário
Ori Pomerantz Ori Pomerantz
13 de fevereiro de 2026
24 minutos de leitura

Neste tutorial, você aprenderá a criar um agente simples de negociação de IA. Este agente funciona seguindo estas etapas:

  1. Ler os preços atuais e passados de um token, bem como outras informações potencialmente relevantes
  2. Criar uma consulta com essas informações, juntamente com informações de fundo para explicar como elas podem ser relevantes
  3. Enviar a consulta e receber de volta um preço projetado
  4. Negociar com base na recomendação
  5. 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))

  1. Se ainda não tiver, baixe e instale o Python (opens in a new tab).

  2. Clone o repositório do GitHub.

    1git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started
    2cd 260215-ai-agent
  3. Instale o uv (opens in a new tab). O comando em seu sistema pode ser diferente.

    1pipx install uv
  4. Baixe as bibliotecas.

    1uv sync
  5. Ative o ambiente virtual.

    1source .venv/bin/activate
  6. Para verificar se o Python e o Web3 estão funcionando corretamente, execute o python3 e forneça a ele este programa. Você pode inseri-lo no prompt >>>; não é necessário criar um arquivo.

    1from web3 import Web3
    2MAINNET_URL = "https://eth.drpc.org"
    3w3 = Web3(Web3.HTTPProvider(MAINNET_URL))
    4w3.eth.block_number
    5quit()

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-quote
2uv run agent.py

Você 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 Web3
2from web3.contract import Contract
3from decimal import Decimal, ROUND_HALF_UP
4from dataclasses import dataclass
5from datetime import datetime, timezone
6from pprint import pprint
7import time
8import functools
9import sys
Exibir tudo

Importe 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 = 12
2MINUTE_BLOCKS = int(60 / BLOCK_TIME_SECONDS)
3HOUR_BLOCKS = MINUTE_BLOCKS * 60
4DAY_BLOCKS = HOUR_BLOCKS * 24

Um 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_BLOCKS

O 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 lendo
2WETHUSDC_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]
6
7ERC20_ABI = [
8 { "name": "symbol", ... },
9 { "name": "decimals", ... }
10]
Exibir tudo

Estas 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: str
4 symbol: str
5 decimals: int
6 contract: Contract

Esta é 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: str
4 token0: ERC20Token
5 token1: ERC20Token
6 contract: Contract
7 asset: str
8 decimal_factor: Decimal = 1

O 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)) ** 2

Para 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 é price&#x22C5296

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: str
4 price: Decimal
5 asset: str

Essa 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()
5
6 return ERC20Token(
7 address=address,
8 symbol=symbol,
9 decimals=decimals,
10 contract=token
11 )
Exibir tudo

Esta 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)
7
8 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 tudo

Esta 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_number

Se 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.asset
7 )

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 quotes

Para 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_BLOCKS
7)
8
9pprint(quotes)
Exibir tudo

Este é 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-prompt
2uv run agent.py

A saída agora será um prompt para um LLM, semelhante a:

1Dadas estas cotações:
2Ativo: WETH/USDC
3 2026-01-20T16:34 3016.21
4 .
5 .
6 .
7 2026-02-01T17:49 2299.10
8
9Ativo: WBTC/WETH
10 2026-01-20T16:34 29.84
11 .
12 .
13 .
14 2026-02-01T17:50 33.46
15
16
17Qual você esperaria que fosse o valor para WETH/USDC no momento 2026-02-02T17:56?
18
19Forneça sua resposta como um único número arredondado para duas casas decimais,
20sem nenhum outro texto.
Exibir tudo

Observe 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.

  1. 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).

  2. A pergunta real. É isso que queremos saber.

  3. 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, timedelta

Precisamos 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 lendo
2WETHUSDC_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 = False
7
8 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_factor
Exibir tudo

No 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 .
5
6 return PoolInfo(
7 .
8 .
9 .
10
11 asset= f"{token1.symbol}/{token0.symbol}" if reverse else f"{token0.symbol}/{token1.symbol}",
12 reverse=reverse
13 )
Exibir tudo

Para 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 result

Esta 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}?
2
3Forneç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)
8
9wethwbtc_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_BLOCKS
15)
Exibir tudo

Revise os dois pools e obtenha cotações de ambos.

1future_time = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat()[0:16]
2
3print(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.

  1. Obtenha uma conta OpenAI (opens in a new tab)

  2. Financie a conta (opens in a new tab) — o valor mínimo no momento da escrita é de US$ 5

  3. Crie uma chave de API (opens in a new tab)

  4. 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>
  5. Faça o checkout e execute o agente

    1git checkout 04-interface-llm
    2uv run agent.py

Aqui está o novo código.

1from openai import OpenAI
2
3open_ai = OpenAI() # O cliente lê a variável de ambiente OPENAI_API_KEY

Importe 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].price
3
4print ("Current price:", wethusdc_quotes[-1].price)
5print(f"In {future_time}, expected price: {expected_price} USD")
6
7if (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 tudo

Mostre 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.py

O resultado esperado é semelhante a:

1Previsão para 2026-01-05T19:50: previsto 3138.93 USD, real 3218.92 USD, erro 79.99 USD
2Previsão para 2026-01-06T19:56: previsto 3243.39 USD, real 3221.08 USD, erro 22.31 USD
3Previsão para 2026-01-07T20:02: previsto 3223.24 USD, real 3146.89 USD, erro 76.35 USD
4Previsão para 2026-01-08T20:11: previsto 3150.47 USD, real 3092.04 USD, erro 58.43 USD
5.
6.
7.
8Previsão para 2026-01-31T22:33: previsto 2637.73 USD, real 2417.77 USD, erro 219.96 USD
9Previsão para 2026-02-01T22:41: previsto 2381.70 USD, real 2318.84 USD, erro 62.86 USD
10Previsão para 2026-02-02T22:49: previsto 2234.91 USD, real 2349.28 USD, erro 114.37 USD
11Erro médio de previsão em 29 previsões: 83.87103448275862068965517241 USD
12Mudança média por recomendação: 4.787931034482758620689655172 USD
13Variância padrão das mudanças: 104.42 USD
14Dias lucrativos: 51.72%
15Dias com perdas: 48.28%
Exibir tudo

A 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 testamos
2
3# Obtenha muitas cotações
4wethusdc_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)
11
12wethwbtc_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_BLOCKS
18)
Exibir tudo

Analisamos CYCLES_FOR_TEST (especificado como 40 aqui) dias para trás.

1# Crie previsões e verifique-as em relação ao histórico real
2
3total_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].price
3 prediction_time_price = wethusdc_quotes[index+CYCLES_BACK-1].price

Obtenha 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 += error
3 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_price
3 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")
2
3length_changes = Decimal(len(changes))
4mean_change = sum(changes, Decimal(0)) / length_changes
5print (f"Mean change per recommendation: {mean_change} USD")
6var = sum((x - mean_change) ** 2 for x in changes) / length_changes
7print (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.

  1. Instale o Foundry (opens in a new tab)

  2. Inicie o anvil (opens in a new tab)

    1anvil --fork-url https://eth.drpc.org --block-time 12

    anvil está 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 comando cast (opens in a new tab) que usamos para manipular a cadeia de blocos.

  3. Ao executar no anvil, há dez contas de teste que têm ETH — defina as variáveis de ambiente para a primeira

    1PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    2ADDRESS=`cast wallet address $PRIVATE_KEY`
  4. 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=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
    2USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    3POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640
    4SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C05861564
    5WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    6USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
  5. 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
  6. Use o SwapRouter para negociar 500 WETH por USDC.

    1cast send $WETH_ADDRESS "approve(address,uint256)" $SWAP_ROUTER 500ether --private-key $PRIVATE_KEY
    2MAXINT=`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_KEY

    A chamada approve cria uma permissão que permite que o SwapRouter gaste alguns dos nossos tokens. Contratos não podem monitorar eventos, portanto, se transferirmos tokens diretamente para o contrato SwapRouter, ele não saberia que foi pago. Em vez disso, permitimos que o contrato SwapRouter gaste uma certa quantia, e então o SwapRouter o faz. Isso é feito através de uma função chamada pelo SwapRouter, para que ele saiba se foi bem-sucedido.

  7. Verifique se você tem o suficiente de ambos os tokens.

    1cast call $WETH_ADDRESS "balanceOf(address)" $ADDRESS | cast from-wei
    2echo `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-trade
2uv run agent.py

A saída será semelhante a:

1(ai-trading-agent) qbzzt@Ori-Cloudnomics:~/260215-ai-agent$ uv run agent.py
2Preço atual: 1843.16
3Em 2026-02-06T23:07, preço esperado: 1724.41 USD
4Saldos da conta antes da negociação:
5Saldo de USDC: 927301.578272
6Saldo de WETH: 500
7Vender, espero que o preço caia 118,75 USD
8Transação de aprovação enviada: 74e367ddbb407c1aaf567d87aa5863049991b1d2aa092b6b85195d925e2bd41f
9Transação de aprovação minerada.
10Transação de venda enviada: fad1bcf938585c9e90364b26ac7a80eea9efd34c37e5db81e58d7655bcae28bf
11Transação de venda minerada.
12Saldos da conta após a negociação:
13Saldo de USDC: 929143.797116
14Saldo de WETH: 499
Exibir tudo

Para realmente usá-lo, você precisa de algumas pequenas alterações.

  • Na linha 14, altere MAINNET_URL para um ponto de acesso real, como https://eth.drpc.org
  • Na linha 28, altere PRIVATE_KEY para 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=1

A 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_ABI
5)

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.")
10
11
12def 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 tudo

As 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()
4
5 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()
3
4if (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()
10
11print("Saldos da conta após a negociação:")
12balances()
Exibir tudo

Este 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 WETH se 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

Este tutorial foi útil?