Pular para o conteúdo principal

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

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

Neste tutorial, você aprenderá a construir um agente de IA de negociação simples. Este agente funciona usando as seguintes etapas:

  1. Ler os preços atuais e passados de um token, bem como outras informações potencialmente relevantes
  2. Construir uma consulta com essas informações, juntamente com informações de contexto 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 produza 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, potencialmente usando informações e algoritmos que o desenvolvedor nem sequer 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 amplamente usada para IA é Python (opens in a new tab), então a usaremos aqui. Não se preocupe se você não souber Python. A linguagem é muito clara e explicarei exatamente o que ela faz.

A biblioteca Web3 (opens in a new tab) é a API do Ethereum mais comum em Python. Ela é bem fácil de usar.

Negociando na blockchain

Existem muitas corretoras descentralizadas (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 usada 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 (LLM), escolhi começar com a OpenAI (opens in a new tab). Para executar o aplicativo 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, prosseguiremos em etapas. Cada etapa é uma ramificação (branch) no GitHub.

Começando

Aqui estão as etapas para começar no UNIX ou Linux (incluindo WSL (opens in a new tab))

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

  2. Clone o repositório do GitHub.

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

    pipx install uv
    
  4. Baixe as bibliotecas.

    uv sync
    
  5. Ative o ambiente virtual.

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

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

Lendo da blockchain

O próximo passo é ler da blockchain. Para fazer isso, você precisa mudar para a ramificação 02-read-quote e então usar uv para executar o programa.

git checkout 02-read-quote
uv run agent.py

Você deve receber uma lista de objetos Quote, cada um com um carimbo de data/hora (timestamp), um preço e o ativo (atualmente sempre WETH/USDC).

Aqui está uma explicação linha por linha.

Importe as bibliotecas que precisamos. Elas são explicadas abaixo quando usadas.

print = functools.partial(print, flush=True)

Substitui o print do Python por uma versão que sempre libera (flushes) 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ída de depuração.

MAINNET_URL = "https://eth.drpc.org"

Uma URL para acessar a Mainnet. Você pode obter uma de um Nó como serviço ou usar uma daquelas anunciadas na Chainlist (opens in a new tab).

BLOCK_TIME_SECONDS = 12
MINUTE_BLOCKS = int(60 / BLOCK_TIME_SECONDS)
HOUR_BLOCKS = MINUTE_BLOCKS * 60
DAY_BLOCKS = HOUR_BLOCKS * 24

Um bloco da Mainnet do Ethereum normalmente acontece a cada doze segundos, então este é o número de blocos que esperaríamos que acontecessem em um período de tempo. Note que este não é um número exato. Quando o propositor de bloco está inativo, esse bloco é ignorado e o tempo para o próximo bloco é de 24 segundos. Se quiséssemos obter o bloco exato para um carimbo de data/hora, 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.

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

# O endereço do pool que estamos lendo
WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")

Os valores de cotação são retirados do pool USDC/WETH do Uniswap v3 no endereço 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 (opens in a new tab). Este endereço já está na forma de checksum, mas é melhor usar Web3.to_checksum_address (opens in a new tab) para tornar o código reutilizável.

Estas são as ABIs (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.

w3 = Web3(Web3.HTTPProvider(MAINNET_URL))

Inicie a biblioteca Web3 (opens in a new tab) e conecte-se a um nó do Ethereum.

@dataclass(frozen=True)
class ERC20Token:
    address: str
    symbol: str
    decimals: int
    contract: Contract

Esta é uma maneira de criar uma classe de dados (data class) 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 a primeira letra maiúscula. Esta classe de dados é frozen, 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.

@dataclass(frozen=True)
class PoolInfo:
    address: str
    token0: ERC20Token
    token1: ERC20Token
    contract: Contract
    asset: str
    decimal_factor: Decimal = 1

O tipo Decimal (opens in a new tab) é usado para lidar com precisão com frações decimais.

    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 chamou aqui. Aqui há outro parâmetro, o número do bloco.

        assert block <= w3.eth.block_number, "Block is in the future"

Se pudéssemos ler o futuro, não precisaríamos de IA para negociação.

        sqrt_price_x96 = Decimal(self.contract.functions.slot0().call(block_identifier=block)[0])

A sintaxe para chamar uma função na EVM a partir da Web3 é esta: <contract object>.functions.<function name>().call(<parameters>). 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 blockchain. Aqui usamos um, block_identifier, para especificar o número do bloco no qual desejamos executar.

O resultado é esta struct, em forma de array (opens in a new tab). O primeiro valor é uma função da taxa de câmbio entre os dois tokens.

        raw_price = (sqrt_price_x96 / Decimal(2**96)) ** 2

Para reduzir os cálculos onchain, 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 é price296

         # (token1 por token0)
        return 1/(raw_price * self.decimal_factor)

O preço bruto que obtemos é o número de token0 que recebemos para cada token1. Em nosso pool, token0 é USDC (stablecoin com o mesmo valor de um dólar americano) e 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.

@dataclass(frozen=True)
class Quote:
    timestamp: str
    price: Decimal
    asset: str

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

Esta função recebe um endereço e retorna informações sobre o contrato do token nesse endereço. Para criar um novo Contract da Web3 (opens in a new tab), fornecemos o endereço e a ABI para w3.eth.contract.

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

def get_quote(pool: PoolInfo, block_number: int = None) -> Quote:

Obtenha um objeto Quote. O valor padrão para block_number é None (nenhum valor).

    if block_number is None:
        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. Esta é a sintaxe para uma instrução if (opens in a new tab).

Pode parecer que teria sido 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.

    block = w3.eth.get_block(block_number)
    price = pool.get_price(block_number)
    return Quote(
        timestamp=datetime.fromtimestamp(block.timestamp, timezone.utc).isoformat(),
        price=price.quantize(Decimal("0.01")),
        asset=pool.asset
    )

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.

def 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>].

    quotes = []
    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 blocos para encontrar cotações vem de range (opens in a new tab).

        quote = get_quote(pool, block)
        quotes.append(quote)
    return quotes

Para cada número de bloco, obtenha um objeto Quote e anexe-o à lista quotes. Em seguida, retorne essa lista.

Este é o código principal do script. Leia as informações do pool, obtenha doze cotações e pprint (opens in a new tab) (imprima-as).

Criando um prompt

Em seguida, precisamos converter esta lista de cotações em um prompt para um LLM e obter um valor futuro esperado.

git checkout 03-create-prompt
uv run agent.py

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

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ções. Os LLMs têm muitas informações de seu treinamento, mas geralmente não têm as mais recentes. Esta é a razão pela qual precisamos recuperar as cotações mais recentes aqui. Adicionar informações a um prompt é chamado de geração aumentada de recuperação (RAG) (opens in a new tab).

  2. A pergunta real. Isso é o 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.

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

# Os endereços dos pools que estamos lendo
WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")
WETHWBTC_ADDRESS = Web3.to_checksum_address("0xCBCdF9626bC03E24f779434178A73a0B4bad62eD")

Temos dois pools que precisamos ler.

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 é o Bitcoin empacotado). Precisamos rastrear se a proporção do pool precisa ser invertida.

Para saber se um pool precisa ser invertido, obtemos 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>.

def format_quotes(quotes: list[Quote]) -> str:
    result = f"Asset: {quotes[0].asset}\n"
    for quote in quotes:
        result += f"\t{quote.timestamp[0:16]} {quote.price.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)}\n"
    return result

Esta função constrói uma string que formata uma lista de objetos Quote, assumindo que todos se aplicam ao mesmo ativo.

def make_prompt(quotes: list[list[Quote]], expected_time: str, asset: str) -> str:
    return f"""

Em Python, literais de string de várias linhas (opens in a new tab) são escritos como """ .... """.

Given these quotes:
{
    functools.reduce(lambda acc, q: acc + '\n' + q,
        map(lambda q: format_quotes(q), quotes))
}

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.

What would you expect the value for {asset} to be at time {expected_time}?

Provide your answer as a single number rounded to two decimal places,
without any other text.
    """

O restante do prompt é como esperado.

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

future_time = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat()[0:16]

print(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.

Interagindo com um LLM

Em seguida, solicitamos a um LLM real e recebemos um valor futuro esperado. Escrevi este programa usando a OpenAI, então se você quiser usar um provedor diferente, precisará ajustá-lo.

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

  2. Adicione fundos à conta (opens in a new tab) — o valor mínimo no momento da redação é 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

    export OPENAI_API_KEY=sk-<the rest of the key goes here>
    
  5. Faça o checkout e execute o agente

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

Aqui está o novo código.

from openai import OpenAI

open_ai = OpenAI()  # O cliente lê a variável de ambiente OPENAI_API_KEY

Importe e instancie a API da OpenAI.

response = open_ai.chat.completions.create(
    model="gpt-4-turbo",
    messages=[
        {"role": "user", "content": prompt}
    ],
    temperature=0.0,
    max_tokens=16,
)

Chame a API da OpenAI (open_ai.chat.completions.create) para criar a resposta.

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

uv run test-predictor.py

O resultado esperado é semelhante a:

A maior parte do testador é idêntica ao agente, mas aqui estão as partes que são novas ou modificadas.

Olhamos para CYCLES_FOR_TEST (especificado como 40 aqui) dias atrás.

# Criar previsões e verificá-las contra o histórico real

total_error = Decimal(0)
changes = []

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 lembrar o propósito do agente. Não é prever a proporção WETH/USDC (preço do ETH). É emitir recomendações de venda e compra. Se o preço for atualmente de US$ 2.000 e ele prever US$ 2.010 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.

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

    wethusdc_slice = wethusdc_quotes[index:index+CYCLES_BACK]
    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 de obter uma previsão que temos no agente.

    predicted_price = Decimal(response.choices[0].message.content.strip())
    real_price = wethusdc_quotes[index+CYCLES_BACK].price
    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 era comprar ou vender.

    error = abs(predicted_price - real_price)
    total_error += error
    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.

    recomended_action = 'buy' if predicted_price > prediction_time_price else 'sell'
    price_increase = real_price - prediction_time_price
    changes.append(price_increase if recomended_action == 'buy' else -price_increase)

Para changes, queremos o impacto monetário de comprar ou vender um ETH. Então, primeiro, precisamos determinar a recomendação, depois avaliar como o preço real mudou e se a recomendação rendeu dinheiro (mudança positiva) ou custou dinheiro (mudança negativa).

print (f"Mean prediction error over {len(wethusdc_quotes)-CYCLES_BACK} predictions: {total_error / Decimal(len(wethusdc_quotes)-CYCLES_BACK)} USD")

length_changes = Decimal(len(changes))
mean_change = sum(changes, Decimal(0)) / length_changes
print (f"Mean change per recommendation: {mean_change} USD")
var = sum((x - mean_change) ** 2 for x in changes) / length_changes
print (f"Standard variance of changes: {var.sqrt().quantize(Decimal("0.01"))} USD")

Relate os resultados.

print (f"Profitable days: {len(list(filter(lambda x: x > 0, changes)))/length_changes:.2%}")
print (f"Losing days: {len(list(filter(lambda x: x < 0, changes)))/length_changes:.2%}")

Use filter (opens in a new tab) para contar o número de dias lucrativos e o número de dias custosos. O resultado é um objeto de filtro, que precisamos converter em uma lista para obter o comprimento.

Enviando transações

Agora precisamos realmente enviar transações. No entanto, não quero gastar dinheiro real neste momento, antes que o sistema seja comprovado. Em vez disso, criaremos uma bifurcação local da Mainnet 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)

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

    O anvil está escutando na URL padrão do 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 blockchain.

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

    PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    ADDRESS=`cast wallet address $PRIVATE_KEY`
    
  4. Estes são os contratos que precisamos usar. SwapRouter (opens in a new tab) é o contrato do Uniswap v3 que usamos para realmente negociar. 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.

    WETH_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
    USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640
    SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C05861564
    WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
    
  5. Cada uma das contas de teste tem 10.000 ETH. Use o contrato WETH para empacotar 1.000 ETH para obter 1.000 WETH para negociação.

    cast send $WETH_ADDRESS "deposit()" --value 1000ether --private-key $PRIVATE_KEY
    
  6. Use SwapRouter para trocar 500 WETH por USDC.

    cast send $WETH_ADDRESS "approve(address,uint256)" $SWAP_ROUTER 500ether --private-key $PRIVATE_KEY
    MAXINT=`cast max-int uint256`
    cast send $SWAP_ROUTER \
        "exactInput((bytes,address,uint256,uint256,uint256))" \
        "($WETH_TO_USDC,$ADDRESS,$MAXINT,500ether,1000000)" \
        --private-key $PRIVATE_KEY
    

    A chamada approve cria uma permissão que permite que SwapRouter gaste alguns de nossos tokens. Os contratos não podem monitorar eventos, então 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 SwapRouter o faz. Isso é feito através de uma função chamada por SwapRouter, para que ele saiba se foi bem-sucedido.

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

    cast call $WETH_ADDRESS "balanceOf(address)" $ADDRESS | cast from-wei
    echo `cast call $USDC_ADDRESS "balanceOf(address)" $ADDRESS | cast to-dec`/10^6 | bc
    

Agora que temos WETH e USDC, podemos realmente executar o agente.

git checkout 05-trade
uv run agent.py

A saída será semelhante a:

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

  • Na linha 14, mude MAINNET_URL para um ponto de acesso real, como https://eth.drpc.org
  • Na linha 28, mude 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 mudar a linha 29 para diminuir WETH_TRADE_AMOUNT

Explicação do código

Aqui está o novo código.

SWAP_ROUTER_ADDRESS=Web3.to_checksum_address("0xE592427A0AEce92De3Edee1F18E0157C05861564")
WETH_TO_USDC=bytes.fromhex("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
USDC_TO_WETH=bytes.fromhex("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"

As mesmas variáveis que usamos na etapa 4.

WETH_TRADE_AMOUNT=1

O valor a ser negociado.

ERC20_ABI = [
    { "name": "symbol", ... },
    { "name": "decimals", ... },
    { "name": "balanceOf", ...},
    { "name": "approve", ...}
]

Para realmente negociar, precisamos da função approve. Também queremos mostrar os saldos antes e depois, então também precisamos de balanceOf.

SWAP_ROUTER_ABI = [
  { "name": "exactInput", ...},
]

Na ABI de SwapRouter, precisamos apenas de exactInput. Há uma função relacionada, exactOutput, que poderíamos usar para comprar exatamente um WETH, mas por simplicidade usamos apenas exactInput em ambos os casos.

account = w3.eth.account.from_key(PRIVATE_KEY)
swap_router = w3.eth.contract(
    address=SWAP_ROUTER_ADDRESS,
    abi=SWAP_ROUTER_ABI
)

As definições da Web3 para o account (opens in a new tab) e o contrato SwapRouter.

def txn_params() -> dict:
    return {
        "from": account.address,
        "value": 0,
        "gas": 300000,
        "nonce": w3.eth.get_transaction_count(account.address),
    }

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.

def approve_token(contract: Contract, amount: int):

Aprove uma permissão de token para SwapRouter.

    txn = contract.functions.approve(SWAP_ROUTER_ADDRESS, amount).build_transaction(txn_params())
    signed_txn = w3.eth.account.sign_transaction(txn, private_key=PRIVATE_KEY)
    tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction)

É assim que enviamos uma transação na 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.

    print(f"Approve transaction sent: {tx_hash.hex()}")
    w3.eth.wait_for_transaction_receipt(tx_hash)
    print("Approve transaction mined.")

w3.eth.wait_for_transaction_receipt (opens in a new tab) aguarda até que a transação seja minerada. Ele retorna o recibo, se necessário.

SELL_PARAMS = {
    "path": WETH_TO_USDC,
    "recipient": account.address,
    "deadline": 2**256 - 1,
    "amountIn": WETH_TRADE_AMOUNT * 10 ** wethusdc_pool.token1.decimals,
    "amountOutMinimum": 0,
}

Estes são os parâmetros ao vender WETH.

def make_buy_params(quote: Quote) -> dict:
    return {
        "path": USDC_TO_WETH,
        "recipient": account.address,
        "deadline": 2**256 - 1,
        "amountIn": int(quote.price*WETH_TRADE_AMOUNT) * 10**wethusdc_pool.token0.decimals,
        "amountOutMinimum": 0,
    }

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.

As funções buy() e sell() são quase idênticas. Primeiro aprovamos uma permissão suficiente para SwapRouter e, em seguida, a chamamos com o caminho e o valor corretos.

def balances():
    token0_balance = wethusdc_pool.token0.contract.functions.balanceOf(account.address).call()
    token1_balance = wethusdc_pool.token1.contract.functions.balanceOf(account.address).call()

    print(f"{wethusdc_pool.token0.symbol} Balance: {Decimal(token0_balance) / Decimal(10 ** wethusdc_pool.token0.decimals)}")
    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.

Este agente atualmente funciona apenas 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 aguardar até que seja a hora do próximo ciclo.

Possíveis melhorias

Esta não é uma versão de produção completa; é meramente 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 a expectativa for de queda no preço, independentemente da magnitude da queda. Pode-se argumentar que seria melhor ignorar pequenas mudanças 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 aos seus LLMs, o que pode 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 esse 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 o autoaperfeiçoamento, 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 derrapagem

Atualmente não há proteção contra derrapagem (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 fará mais sentido comprar.

Para implementar a proteção contra derrapagem, especifique um valor 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 com agentes de IA. Esta não é uma visão geral abrangente do assunto; há livros inteiros dedicados a isso, mas isso é o suficiente para você começar. Boa sorte!

Veja aqui mais do meu trabalho (opens in a new tab).