Ir al contenido principal

Crea tu propio agente de negociación con IA en Ethereum

IA
negociación
agente
python
Intermedio
Ori Pomerantz
13 de febrero de 2026
25 minuto leído

En este tutorial, aprenderás a crear un agente de negociación con IA simple. Este agente funciona siguiendo estos pasos:

  1. Lee los precios actuales y pasados de un token, así como otra información potencialmente relevante
  2. Crea una consulta con esta información, junto con información de fondo para explicar cómo podría ser relevante
  3. Envía la consulta y recibe un precio proyectado
  4. Opera en función de la recomendación
  5. Espera y repite

Este agente demuestra cómo leer información, traducirla en una consulta que produzca una respuesta útil y utilizar esa respuesta. Todos estos son pasos necesarios para un agente de IA. Este agente está implementado en Python porque es el lenguaje más común utilizado en IA.

¿Por qué hacer esto?

Los agentes de negociación automatizados permiten a los desarrolladores seleccionar y ejecutar una estrategia de negociación. Agentes de IA permiten estrategias de negociación más complejas y dinámicas, utilizando potencialmente información y algoritmos que el desarrollador ni siquiera ha considerado usar.

Las herramientas

Este tutorial utiliza Python (opens in a new tab), la biblioteca Web3 (opens in a new tab) y Uniswap v3 (opens in a new tab) para cotizaciones y negociación.

¿Por qué Python?

El lenguaje más utilizado para la IA es Python (opens in a new tab), por lo que lo usamos aquí. No te preocupes si no sabes Python. El lenguaje es muy claro y explicaré exactamente lo que hace.

La biblioteca Web3 (opens in a new tab) es la API de Python para Ethereum más común. Es bastante fácil de usar.

Operar en la cadena de bloques

Existen muchos intercambios descentralizados (DEX) que te permiten operar con tokens en Ethereum. Sin embargo, tienden a tener tasas de cambio similares debido al arbitraje.

Uniswap (opens in a new tab) es un DEX muy utilizado que podemos usar tanto para cotizaciones (para ver los valores relativos de los tokens) como para operaciones.

OpenAI

Para un modelo de lenguaje grande, elegí empezar con OpenAI (opens in a new tab). Para ejecutar la aplicación de este tutorial, necesitarás pagar por el acceso a la API. El pago mínimo de 5 $ es más que suficiente.

Desarrollo, paso a paso

Para simplificar el desarrollo, procederemos por etapas. Cada paso es una rama en GitHub.

Primeros pasos

Estos son los pasos para empezar en UNIX o Linux (incluido WSL (opens in a new tab))

  1. Si aún no lo tienes, descarga e instala Python (opens in a new tab).

  2. Clone el repositorio de GitHub.

    1git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started
    2cd 260215-ai-agent
  3. Instala uv (opens in a new tab). El comando en tu sistema podría ser diferente.

    1pipx install uv
  4. Descarga las bibliotecas.

    1uv sync
  5. Activa el entorno virtual.

    1source .venv/bin/activate
  6. Para verificar que Python y Web3 funcionan correctamente, ejecuta python3 y proporciónale este programa. Puedes introducirlo en el indicador >>>; no es necesario crear un archivo.

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

Lectura desde la cadena de bloques

El siguiente paso es leer desde la cadena de bloques. Para ello, debes cambiar a la rama 02-read-quote y luego usar uv para ejecutar el programa.

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

Deberías recibir una lista de objetos Quote, cada uno con una marca de tiempo, un precio y el activo (actualmente siempre WETH/USDC).

Aquí tienes una explicación línea por línea.

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
Mostrar todo

Importe las librerías que necesitamos. Se explican a continuación cuando se utilizan.

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

Reemplaza el print de Python con una versión que siempre vacía la salida inmediatamente. Esto es útil en un script de larga duración porque no queremos esperar a las actualizaciones de estado ni a la salida de depuración.

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

Una URL para acceder a la red principal. Puedes obtener una de Nodo como servicio o usar una de las anunciadas en 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

Un bloque de la red principal de Ethereum normalmente se produce cada doce segundos, así que este es el número de bloques que esperaríamos que se produjeran en un período de tiempo. Ten en cuenta que esta no es una cifra exacta. Cuando el proponente de bloque está inactivo, ese bloque se salta y el tiempo para el siguiente bloque es de 24 segundos. Si quisiéramos obtener el bloque exacto para una marca de tiempo, usaríamos la búsqueda binaria (opens in a new tab). Sin embargo, esto es lo suficientemente aproximado para nuestros propósitos. Predecir el futuro no es una ciencia exacta.

1CYCLE_BLOCKS = DAY_BLOCKS

El tamaño del ciclo. Revisamos las cotizaciones una vez por ciclo e intentamos estimar el valor al final del siguiente ciclo.

1# La dirección del fondo de liquidez que estamos leyendo
2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")

Los valores de cotización se toman del fondo de liquidez Uniswap 3 USDC/WETH en la dirección 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 (opens in a new tab). Esta dirección ya está en formato de suma de verificación, pero es mejor usar Web3.to_checksum_address (opens in a new tab) para que el código sea reutilizable.

1POOL_ABI = [
2 { "name": "slot0", ... },
3 { "name": "token0", ... },
4 { "name": "token1", ... },
5]
6
7ERC20_ABI = [
8 { "name": "symbol", ... },
9 { "name": "decimals", ... }
10]
Mostrar todo

Estas son las ABI (opens in a new tab) para los dos contratos que necesitamos contactar. Para mantener el código conciso, incluimos solo las funciones que necesitamos llamar.

1w3 = Web3(Web3.HTTPProvider(MAINNET_URL))

Inicia la biblioteca Web3 (opens in a new tab) y conéctate a un nodo de Ethereum.

1@dataclass(frozen=True)
2class ERC20Token:
3 address: str
4 symbol: str
5 decimals: int
6 contract: Contract

Esta es una forma de crear una clase de datos en Python. El tipo de datos Contract (opens in a new tab) se usa para conectarse al contrato. Observa el (frozen=True). En Python, los booleanos (opens in a new tab) se definen como True o False, con mayúscula inicial. Esta clase de datos es frozen (congelada), lo que significa que los campos no se pueden modificar.

Observa la sangría. A diferencia de los lenguajes derivados de C (opens in a new tab), Python utiliza la sangría para denotar bloques. El intérprete de Python sabe que la siguiente definición no forma parte de esta clase de datos porque no comienza con la misma sangría que los campos de la clase de datos.

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

El tipo Decimal (opens in a new tab) se utiliza para manejar con precisión las fracciones decimales.

1 def get_price(self, block: int) -> Decimal:

Esta es la forma de definir una función en Python. La definición está sangrada para mostrar que todavía es parte de PoolInfo.

En una función que forma parte de una clase de datos, el primer parámetro es siempre self, la instancia de la clase de datos que la llamó. Aquí hay otro parámetro, el número de bloque.

1 assert block <= w3.eth.block_number, "El bloque está en el futuro"

Si pudiéramos leer el futuro, no necesitaríamos IA para operar.

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

La sintaxis para llamar a una función en la EVM desde Web3 es esta: <objeto de contrato>.functions.<nombre de función>"().call(<parámetros>). Los parámetros pueden ser los parámetros de la función de la EVM (si los hay; aquí no hay) o parámetros con nombre (opens in a new tab) para modificar el comportamiento de la cadena de bloques. Aquí usamos uno, block_identifier, para especificar el número de bloque en el que deseamos ejecutar.

El resultado es esta estructura, en forma de matriz (opens in a new tab). El primer valor es una función de la tasa de cambio entre los dos tokens.

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

Para reducir los cálculos en la cadena, Uniswap v3 no almacena el factor de cambio real, sino su raíz cuadrada. Debido a que la EVM no admite matemáticas de punto flotante ni fracciones, en lugar del valor real, la respuesta es price&#x22C5296

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

El precio bruto que obtenemos es el número de token0 que obtenemos por cada token1. En nuestro fondo de liquidez, token0 es USDC (una moneda estable con el mismo valor que un dólar estadounidense) y token1 es WETH (opens in a new tab). El valor que realmente queremos es el número de dólares por WETH, no el inverso.

El factor decimal es la relación entre los factores decimales (opens in a new tab) de los dos tokens.

1@dataclass(frozen=True)
2class Quote:
3 timestamp: str
4 price: Decimal
5 asset: str

Esta clase de datos representa una cotización: el precio de un activo específico en un momento dado. En este punto, el campo asset es irrelevante porque usamos un único fondo de liquidez y, por lo tanto, tenemos un único activo. Sin embargo, añadiremos más activos más adelante.

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 )
Mostrar todo

Esta función toma una dirección y devuelve información sobre el contrato del token en esa dirección. Para crear un nuevo Contrato de Web3 (opens in a new tab), proporcionamos la dirección y la ABI a 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 )
Mostrar todo

Esta función devuelve todo lo que necesitamos sobre un fondo de liquidez específico (opens in a new tab). La sintaxis f"<string>" es una cadena formateada (opens in a new tab).

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

Obtiene un objeto Quote. El valor predeterminado para block_number es None (sin valor).

1 if block_number is None:
2 block_number = w3.eth.block_number

Si no se especificó un número de bloque, usa w3.eth.block_number, que es el último número de bloque. Esta es la sintaxis para una instrucción if (opens in a new tab).

Podría parecer que hubiera sido mejor simplemente establecer el valor predeterminado en w3.eth.block_number, pero eso no funciona bien porque sería el número de bloque en el momento en que se define la función. En un agente de larga duración, esto sería un 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 )

Usa la biblioteca datetime (opens in a new tab) para darle un formato legible para humanos y modelos de lenguaje grandes (LLM). Usa Decimal.quantize (opens in a new tab) para redondear el valor a dos decimales.

1def get_quotes(pool: PoolInfo, start_block: int, end_block: int, step: int) -> list[Quote]:

En Python, se define una lista (opens in a new tab) que solo puede contener un tipo específico usando list[<tipo>].

1 quotes = []
2 for block in range(start_block, end_block + 1, step):

En Python, un bucle for (opens in a new tab) normalmente itera sobre una lista. La lista de números de bloque en los que buscar cotizaciones proviene 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 bloque, obtén un objeto Quote y añádelo a la lista quotes. Luego, devuelve esa 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)
Mostrar todo

Este es el código principal del script. Lee la información del fondo de liquidez, obtén doce cotizaciones y usa pprint (opens in a new tab) para imprimirlas.

Crear una indicación

A continuación, necesitamos convertir esta lista de cotizaciones en una indicación para un LLM y obtener un valor futuro esperado.

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

La salida ahora será una indicación para un LLM, similar a:

1Dadas estas cotizaciones:
2Activo: WETH/USDC
3 2026-01-20T16:34 3016.21
4 .
5 .
6 .
7 2026-02-01T17:49 2299.10
8
9Activo: WBTC/WETH
10 2026-01-20T16:34 29.84
11 .
12 .
13 .
14 2026-02-01T17:50 33.46
15
16
17¿Qué valor esperarías que tuviera WETH/USDC en la fecha 2026-02-02T17:56?
18
19Proporciona tu respuesta como un único número redondeado a dos decimales,
20sin ningún otro texto.
Mostrar todo

Observa que aquí hay cotizaciones para dos activos, WETH/USDC y WBTC/WETH. Añadir cotizaciones de otro activo podría mejorar la precisión de la predicción.

Cómo es una indicación

Esta indicación contiene tres secciones, que son bastante comunes en las indicaciones para LLM.

  1. Información. Los LLM tienen mucha información de su entrenamiento, pero generalmente no tienen la más reciente. Esta es la razón por la que necesitamos recuperar las últimas cotizaciones aquí. Añadir información a una indicación se llama generación aumentada por recuperación (RAG) (opens in a new tab).

  2. La pregunta real. Esto es lo que queremos saber.

  3. Instrucciones de formato de salida. Normalmente, un LLM nos dará una estimación con una explicación de cómo llegó a ella. Esto es mejor para los humanos, pero un programa informático solo necesita el resultado final.

Explicación del código

Este es el nuevo código.

1from datetime import datetime, timezone, timedelta

Necesitamos proporcionar al LLM la hora para la que queremos una estimación. Para obtener una hora "n minutos/horas/días" en el futuro, usamos la clase timedelta (opens in a new tab).

1# Las direcciones de los fondos de liquidez que estamos leyendo
2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")
3WETHWBTC_ADDRESS = Web3.to_checksum_address("0xCBCdF9626bC03E24f779434178A73a0B4bad62eD")

Tenemos dos fondos de liquidez que necesitamos leer.

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, "El bloque está en el futuro"
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 por token0)
12 if self.reverse:
13 return 1/(raw_price * self.decimal_factor)
14 else:
15 return raw_price * self.decimal_factor
Mostrar todo

En el fondo de liquidez WETH/USDC, queremos saber cuántos de token0 (USDC) necesitamos para comprar uno de token1 (WETH). En el fondo de liquidez WETH/WBTC, queremos saber cuántos token1 (WETH) necesitamos para comprar un token0 (WBTC, que es Bitcoin envuelto). Necesitamos hacer un seguimiento de si la proporción del fondo de liquidez debe invertirse.

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 )
Mostrar todo

Para saber si un fondo de liquidez debe invertirse, debemos obtener eso como entrada para read_pool. Además, el símbolo del activo debe configurarse correctamente.

La sintaxis <a> if <b> else <c> es el equivalente en Python del operador condicional ternario (opens in a new tab), que en un lenguaje derivado de C sería <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 función crea una cadena que formatea una lista de objetos Quote, asumiendo que todos se aplican al mismo activo.

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

En Python, los literales de cadena multilínea (opens in a new tab) se escriben como """ .... """.

1Dadas estas cotizaciones:
2{
3 functools.reduce(lambda acc, q: acc + '\n' + q,
4 map(lambda q: format_quotes(q), quotes))
5}

Aquí, usamos el patrón MapReduce (opens in a new tab) para generar una cadena para cada lista de cotizaciones con format_quotes, y luego las reducimos a una sola cadena para usarla en la indicación.

1¿Qué valor esperarías que tuviera {asset} en la fecha {expected_time}?
2
3Proporciona tu respuesta como un único número redondeado a dos decimales,
4sin ningún otro texto.
5 """

El resto de la indicación es como se esperaba.

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)
Mostrar todo

Revisa los dos fondos de liquidez y obtén cotizaciones 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))

Determina el punto de tiempo futuro para el que queremos la estimación y crea la indicación.

Interfaz con un LLM

A continuación, le daremos una indicación a un LLM real y recibiremos un valor futuro esperado. Escribí este programa usando OpenAI, así que si quieres usar un proveedor diferente, tendrás que ajustarlo.

  1. Obtén una cuenta de OpenAI (opens in a new tab)

  2. Financia la cuenta (opens in a new tab): la cantidad mínima en el momento de escribir este artículo es de 5 $

  3. Crea una clave de API (opens in a new tab)

  4. En la línea de comandos, exporta la clave de API para que tu programa pueda usarla

    1export OPENAI_API_KEY=sk-<el resto de la clave va aquí>
  5. Haz un «checkout» y ejecuta el agente

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

Este es el nuevo código.

1from openai import OpenAI
2
3open_ai = OpenAI() # El cliente lee la variable de entorno OPENAI_API_KEY

Importa e instancia la API de 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)

Llama a la API de OpenAI (open_ai.chat.completions.create) para crear la respuesta.

1expected_price = Decimal(response.choices[0].message.content.strip())
2current_price = wethusdc_quotes[-1].price
3
4print ("Precio actual:", wethusdc_quotes[-1].price)
5print(f"En {future_time}, precio esperado: {expected_price} USD")
6
7if (expected_price > current_price):
8 print(f"Comprar, espero que el precio suba en {expected_price - current_price} USD")
9else:
10 print(f"Vender, espero que el precio baje en {current_price - expected_price} USD")
Mostrar todo

Muestra el precio y proporciona una recomendación de compra o venta.

Prueba de las predicciones

Ahora que podemos generar predicciones, también podemos usar datos históricos para evaluar si producimos predicciones útiles.

1uv run test-predictor.py

El resultado esperado es similar a:

1Predicción para 2026-01-05T19:50: predicho 3138.93 USD, real 3218.92 USD, error 79.99 USD
2Predicción para 2026-01-06T19:56: predicho 3243.39 USD, real 3221.08 USD, error 22.31 USD
3Predicción para 2026-01-07T20:02: predicho 3223.24 USD, real 3146.89 USD, error 76.35 USD
4Predicción para 2026-01-08T20:11: predicho 3150.47 USD, real 3092.04 USD, error 58.43 USD
5.
6.
7.
8Predicción para 2026-01-31T22:33: predicho 2637.73 USD, real 2417.77 USD, error 219.96 USD
9Predicción para 2026-02-01T22:41: predicho 2381.70 USD, real 2318.84 USD, error 62.86 USD
10Predicción para 2026-02-02T22:49: predicho 2234.91 USD, real 2349.28 USD, error 114.37 USD
11Error de predicción medio en 29 predicciones: 83.87103448275862068965517241 USD
12Cambio medio por recomendación: 4.787931034482758620689655172 USD
13Varianza estándar de los cambios: 104.42 USD
14Días rentables: 51.72%
15Días con pérdidas: 48.28%
Mostrar todo

La mayor parte del probador es idéntica al agente, pero aquí están las partes que son nuevas o modificadas.

1CYCLES_FOR_TEST = 40 # Para la prueba retrospectiva, cuántos ciclos probamos
2
3# Obtener muchas cotizaciones
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)
Mostrar todo

Miramos hacia atrás CYCLES_FOR_TEST (especificado como 40 aquí) días.

1# Crear predicciones y verificarlas con el historial real
2
3total_error = Decimal(0)
4changes = []

Hay dos tipos de errores que nos interesan. El primero, total_error, es simplemente la suma de los errores que cometió el predictor.

Para entender el segundo, changes, debemos recordar el propósito del agente. No es para predecir la relación WETH/USDC (precio de ETH). Es para emitir recomendaciones de compra y venta. Si el precio es actualmente de 2000 $ y predice 2010 $ para mañana, no nos importa si el resultado real es 2020 $ y ganamos dinero extra. Pero nos importa si predijo 2010 $, y compramos ETH basándonos en esa recomendación, y el precio baja a 1990 $.

1for index in range(0,len(wethusdc_quotes)-CYCLES_BACK):

Solo podemos ver los casos en los que el historial completo (los valores utilizados para la predicción y el valor del mundo real para compararlo) está disponible. Esto significa que el caso más reciente debe ser el que comenzó hace CYCLES_BACK.

1 wethusdc_slice = wethusdc_quotes[index:index+CYCLES_BACK]
2 wethwbtc_slice = wethwbtc_quotes[index:index+CYCLES_BACK]

Usa divisiones (opens in a new tab) para obtener el mismo número de muestras que el número que usa el agente. El código entre aquí y el siguiente segmento es el mismo código para obtener una predicción que tenemos en el 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

Obtén el precio previsto, el precio real y el precio en el momento de la predicción. Necesitamos el precio en el momento de la predicción para determinar si la recomendación era comprar o vender.

1 error = abs(predicted_price - real_price)
2 total_error += error
3 print (f"Predicción para {prediction_time}: predicho {predicted_price} USD, real {real_price} USD, error {error} USD")

Calcula el error y súmalo al 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 el impacto monetario de comprar o vender un ETH. Así que primero, necesitamos determinar la recomendación, luego evaluar cómo cambió el precio real y si la recomendación generó ganancias (cambio positivo) o pérdidas (cambio negativo).

1print (f"Error de predicción medio en {len(wethusdc_quotes)-CYCLES_BACK} predicciones: {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"Cambio medio por recomendación: {mean_change} USD")
6var = sum((x - mean_change) ** 2 for x in changes) / length_changes
7print (f"Varianza estándar de los cambios: {var.sqrt().quantize(Decimal("0.01"))} USD")

Informa de los resultados.

1print (f"Días rentables: {len(list(filter(lambda x: x > 0, changes)))/length_changes:.2%}")
2print (f"Días con pérdidas: {len(list(filter(lambda x: x < 0, changes)))/length_changes:.2%}")

Usa filter (opens in a new tab) para contar el número de días rentables y el número de días con pérdidas. El resultado es un objeto de filtro, que debemos convertir en una lista para obtener la longitud.

Envío de transacciones

Ahora necesitamos enviar transacciones reales. Sin embargo, no quiero gastar dinero real en este punto, antes de que el sistema esté probado. En su lugar, crearemos una bifurcación local de la red principal y «operaremos» en esa red.

Aquí están los pasos para crear una bifurcación local y habilitar la negociación.

  1. Instala Foundry (opens in a new tab)

  2. Inicia anvil (opens in a new tab)

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

    anvil está escuchando en la URL predeterminada de Foundry, http://localhost:8545 (opens in a new tab), por lo que no necesitamos especificar la URL para el comando cast (opens in a new tab) que usamos para manipular la cadena de bloques.

  3. Cuando se ejecuta en anvil, hay diez cuentas de prueba que tienen ETH: establece las variables de entorno para la primera

    1PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    2ADDRESS=`cast wallet address $PRIVATE_KEY`
  4. Estos son los contratos que necesitamos usar. SwapRouter (opens in a new tab) es el contrato de Uniswap v3 que usamos para operar realmente. Podríamos operar directamente a través del fondo de liquidez, pero esto es mucho más fácil.

    Las dos variables inferiores son las rutas de Uniswap v3 necesarias para intercambiar entre WETH y USDC.

    1WETH_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
    2USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    3POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640
    4SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C05861564
    5WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    6USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
  5. Cada una de las cuentas de prueba tiene 10.000 ETH. Usa el contrato WETH para envolver 1000 ETH y obtener 1000 WETH para operar.

    1cast send $WETH_ADDRESS "deposit()" --value 1000ether --private-key $PRIVATE_KEY
  6. Usa SwapRouter para intercambiar 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

    La llamada approve crea una asignación que permite a SwapRouter gastar algunos de nuestros tokens. Los contratos no pueden supervisar eventos, por lo que si transferimos tokens directamente al contrato SwapRouter, no sabría que se le ha pagado. En cambio, permitimos que el contrato SwapRouter gaste una cierta cantidad, y luego SwapRouter lo hace. Esto se hace a través de una función llamada por SwapRouter, para que sepa si tuvo éxito.

  7. Verifica que tienes suficientes de ambos 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

Ahora que tenemos WETH y USDC, podemos ejecutar el agente.

1git checkout 05-trade
2uv run agent.py

La salida será similar a:

1(ai-trading-agent) qbzzt@Ori-Cloudnomics:~/260215-ai-agent$ uv run agent.py
2Precio actual: 1843.16
3En 2026-02-06T23:07, precio esperado: 1724.41 USD
4Saldos de la cuenta antes de la operación:
5Saldo de USDC: 927301.578272
6Saldo de WETH: 500
7Vender, espero que el precio baje en 118.75 USD
8Transacción de aprobación enviada: 74e367ddbb407c1aaf567d87aa5863049991b1d2aa092b6b85195d925e2bd41f
9Transacción de aprobación minada.
10Transacción de venta enviada: fad1bcf938585c9e90364b26ac7a80eea9efd34c37e5db81e58d7655bcae28bf
11Transacción de venta minada.
12Saldos de la cuenta después de la operación:
13Saldo de USDC: 929143.797116
14Saldo de WETH: 499
Mostrar todo

Para usarlo realmente, necesitas algunos cambios menores.

  • En la línea 14, cambia MAINNET_URL a un punto de acceso real, como https://eth.drpc.org
  • En la línea 28, cambia PRIVATE_KEY a tu propia clave privada
  • A menos que seas muy rico y puedas comprar o vender 1 ETH cada día para un agente no probado, es posible que quieras cambiar la línea 29 para disminuir WETH_TRADE_AMOUNT

Explicación del código

Este es el nuevo 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"

Las mismas variables que usamos en el paso 4.

1WETH_TRADE_AMOUNT=1

La cantidad a operar.

1ERC20_ABI = [
2 { "name": "symbol", ... },
3 { "name": "decimals", ... },
4 { "name": "balanceOf", ...},
5 { "name": "approve", ...}
6]

Para operar realmente, necesitamos la función approve. También queremos mostrar los saldos antes y después, así que también necesitamos balanceOf.

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

En la ABI SwapRouter, solo necesitamos exactInput. Hay una función relacionada, exactOutput, que podríamos usar para comprar exactamente un WETH, pero por simplicidad solo usamos exactInput en ambos 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)

Las definiciones de Web3 para la cuenta (opens in a new tab) y el 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 }

Los parámetros de la transacción. Necesitamos una función aquí porque el nonce (opens in a new tab) debe cambiar cada vez.

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

Aprobar una asignación de token para 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)

Así es como enviamos una transacción en Web3. Primero usamos el objeto Contract (opens in a new tab) para construir la transacción. Luego usamos web3.eth.account.sign_transaction (opens in a new tab) para firmar la transacción, usando PRIVATE_KEY. Finalmente, usamos w3.eth.send_raw_transaction (opens in a new tab) para enviar la transacción.

1 print(f"Transacción de aprobación enviada: {tx_hash.hex()}")
2 w3.eth.wait_for_transaction_receipt(tx_hash)
3 print("Transacción de aprobación minada.")

w3.eth.wait_for_transaction_receipt (opens in a new tab) espera hasta que la transacción sea minada. Devuelve el recibo si es necesario.

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}

Estos son los parámetros al 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 }

A diferencia de SELL_PARAMS, los parámetros de compra pueden cambiar. La cantidad de entrada es el costo de 1 WETH, como está disponible en 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"Transacción de compra enviada: {tx_hash.hex()}")
8 w3.eth.wait_for_transaction_receipt(tx_hash)
9 print("Transacción de compra minada.")
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"Transacción de venta enviada: {tx_hash.hex()}")
19 w3.eth.wait_for_transaction_receipt(tx_hash)
20 print("Transacción de venta minada.")
Mostrar todo

Las funciones buy() y sell() son casi idénticas. Primero aprobamos una asignación suficiente para SwapRouter y luego lo llamamos con la ruta y la cantidad correctas.

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

Informa de los saldos de los usuarios en ambas monedas.

1print("Saldos de la cuenta antes de la operación:")
2balances()
3
4if (expected_price > current_price):
5 print(f"Comprar, espero que el precio suba en {expected_price - current_price} USD")
6 buy(wethusdc_quotes[-1])
7else:
8 print(f"Vender, espero que el precio baje en {current_price - expected_price} USD")
9 sell()
10
11print("Saldos de la cuenta después de la operación:")
12balances()
Mostrar todo

Este agente actualmente solo funciona una vez. Sin embargo, puedes cambiarlo para que funcione continuamente, ya sea ejecutándolo desde crontab (opens in a new tab) o envolviendo las líneas 368-400 en un bucle y usando time.sleep (opens in a new tab) para esperar hasta que sea el momento del siguiente ciclo.

Posibles mejoras

Esta no es una versión de producción completa; es simplemente un ejemplo para enseñar los conceptos básicos. Aquí hay algunas ideas para mejoras.

Negociación más inteligente

Hay dos hechos importantes que el agente ignora al decidir qué hacer.

  • La magnitud del cambio anticipado. El agente vende una cantidad fija de WETH si se espera que el precio baje, independientemente de la magnitud de la caída. Podría decirse que sería mejor ignorar los cambios menores y vender en función de cuánto esperamos que baje el precio.
  • La cartera actual. Si el 10 % de tu cartera está en WETH y crees que el precio subirá, probablemente tenga sentido comprar más. Pero si el 90 % de tu cartera está en WETH, es posible que ya estés suficientemente expuesto y no haya necesidad de comprar más. Lo contrario es cierto si esperas que el precio baje.

¿Y si quieres mantener tu estrategia de negociación en secreto?

Los proveedores de IA pueden ver las consultas que envías a sus LLM, lo que podría exponer el genial sistema de negociación que desarrollaste con tu agente. Un sistema de negociación que demasiada gente usa no tiene valor porque demasiada gente intenta comprar cuando tú quieres comprar (y el precio sube) e intenta vender cuando tú quieres vender (y el precio baja).

Puedes ejecutar un LLM localmente, por ejemplo, usando LM-Studio (opens in a new tab), para evitar este problema.

De bot de IA a agente de IA

Se puede argumentar que este es un bot de IA, no un agente de IA. Implementa una estrategia relativamente simple que se basa en información predefinida. Podemos habilitar la automejora, por ejemplo, proporcionando una lista de fondos de liquidez de Uniswap v3 y sus últimos valores y preguntando qué combinación tiene el mejor valor predictivo.

Protección contra el deslizamiento

Actualmente no hay protección contra el deslizamiento (opens in a new tab). Si la cotización actual es de 2000 $ y el precio esperado es de 2100 $, el agente comprará. Sin embargo, si antes de que el agente compre el costo sube a 2200 $, ya no tiene sentido comprar.

Para implementar la protección contra el deslizamiento, especifica un valor de amountOutMinimum en las líneas 325 y 334 de agent.py (opens in a new tab).

Conclusión

Con suerte, ahora sabes lo suficiente para empezar con los agentes de IA. Esta no es una descripción exhaustiva del tema; hay libros enteros dedicados a eso, pero esto es suficiente para que empieces. ¡Buena suerte!

Vea aquí más de mi trabajo (opens in a new tab).

Última actualización de la página: 10 de febrero de 2026

¿Le ha resultado útil este tutorial?