Crea tu propio agente de negociación con IA en Ethereum
En este tutorial, aprenderás a crear un agente de negociación con IA simple. Este agente funciona siguiendo estos pasos:
- Lee los precios actuales y pasados de un token, así como otra información potencialmente relevante
- Crea una consulta con esta información, junto con información de fondo para explicar cómo podría ser relevante
- Envía la consulta y recibe un precio proyectado
- Opera en función de la recomendación
- 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))
-
Si aún no lo tienes, descarga e instala Python (opens in a new tab).
-
Clone el repositorio de GitHub.
1git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started2cd 260215-ai-agent -
Instala
uv(opens in a new tab). El comando en tu sistema podría ser diferente.1pipx install uv -
Descarga las bibliotecas.
1uv sync -
Activa el entorno virtual.
1source .venv/bin/activate -
Para verificar que Python y Web3 funcionan correctamente, ejecuta
python3y proporciónale este programa. Puedes introducirlo en el indicador>>>; no es necesario crear un archivo.1from web3 import Web32MAINNET_URL = "https://eth.drpc.org"3w3 = Web3(Web3.HTTPProvider(MAINNET_URL))4w3.eth.block_number5quit()
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-quote2uv run agent.pyDeberí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 Web32from web3.contract import Contract3from decimal import Decimal, ROUND_HALF_UP4from dataclasses import dataclass5from datetime import datetime, timezone6from pprint import pprint7import time8import functools9import sysMostrar todoImporte 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 = 122MINUTE_BLOCKS = int(60 / BLOCK_TIME_SECONDS)3HOUR_BLOCKS = MINUTE_BLOCKS * 604DAY_BLOCKS = HOUR_BLOCKS * 24Un 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_BLOCKSEl 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 leyendo2WETHUSDC_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]67ERC20_ABI = [8 { "name": "symbol", ... },9 { "name": "decimals", ... }10]Mostrar todoEstas 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: str4 symbol: str5 decimals: int6 contract: ContractEsta 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: str4 token0: ERC20Token5 token1: ERC20Token6 contract: Contract7 asset: str8 decimal_factor: Decimal = 1El 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)) ** 2Para 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
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: str4 price: Decimal5 asset: strEsta 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()56 return ERC20Token(7 address=address,8 symbol=symbol,9 decimals=decimals,10 contract=token11 )Mostrar todoEsta 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)78 return PoolInfo(9 address=address,10 asset=f"{token1.symbol}/{token0.symbol}",11 token0=token0,12 token1=token1,13 contract=pool_contract,14 decimal_factor=Decimal(10) ** Decimal(token0.decimals - token1.decimals)15 )Mostrar todoEsta 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_numberSi 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.asset7 )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 quotesPara 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_BLOCKS7)89pprint(quotes)Mostrar todoEste 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-prompt2uv run agent.pyLa salida ahora será una indicación para un LLM, similar a:
1Dadas estas cotizaciones:2Activo: WETH/USDC3 2026-01-20T16:34 3016.214 .5 .6 .7 2026-02-01T17:49 2299.1089Activo: WBTC/WETH10 2026-01-20T16:34 29.8411 .12 .13 .14 2026-02-01T17:50 33.46151617¿Qué valor esperarías que tuviera WETH/USDC en la fecha 2026-02-02T17:56?1819Proporciona tu respuesta como un único número redondeado a dos decimales,20sin ningún otro texto.Mostrar todoObserva 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.
-
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).
-
La pregunta real. Esto es lo que queremos saber.
-
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, timedeltaNecesitamos 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 leyendo2WETHUSDC_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 = False78 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_factorMostrar todoEn 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 .56 return PoolInfo(7 .8 .9 .1011 asset= f"{token1.symbol}/{token0.symbol}" if reverse else f"{token0.symbol}/{token1.symbol}",12 reverse=reverse13 )Mostrar todoPara 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 resultEsta 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}?23Proporciona 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)89wethwbtc_pool = read_pool(WETHWBTC_ADDRESS)10wethwbtc_quotes = get_quotes(11 wethwbtc_pool,12 w3.eth.block_number - 12*CYCLE_BLOCKS,13 w3.eth.block_number,14 CYCLE_BLOCKS15)Mostrar todoRevisa los dos fondos de liquidez y obtén cotizaciones de ambos.
1future_time = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat()[0:16]23print(make_prompt(wethusdc_quotes + wethwbtc_quotes, future_time, wethusdc_pool.asset))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.
-
Obtén una cuenta de OpenAI (opens in a new tab)
-
Financia la cuenta (opens in a new tab): la cantidad mínima en el momento de escribir este artículo es de 5 $
-
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í> -
Haz un «checkout» y ejecuta el agente
1git checkout 04-interface-llm2uv run agent.py
Este es el nuevo código.
1from openai import OpenAI23open_ai = OpenAI() # El cliente lee la variable de entorno OPENAI_API_KEYImporta 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].price34print ("Precio actual:", wethusdc_quotes[-1].price)5print(f"En {future_time}, precio esperado: {expected_price} USD")67if (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 todoMuestra 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.pyEl resultado esperado es similar a:
1Predicción para 2026-01-05T19:50: predicho 3138.93 USD, real 3218.92 USD, error 79.99 USD2Predicción para 2026-01-06T19:56: predicho 3243.39 USD, real 3221.08 USD, error 22.31 USD3Predicción para 2026-01-07T20:02: predicho 3223.24 USD, real 3146.89 USD, error 76.35 USD4Predicción para 2026-01-08T20:11: predicho 3150.47 USD, real 3092.04 USD, error 58.43 USD5.6.7.8Predicción para 2026-01-31T22:33: predicho 2637.73 USD, real 2417.77 USD, error 219.96 USD9Predicción para 2026-02-01T22:41: predicho 2381.70 USD, real 2318.84 USD, error 62.86 USD10Predicción para 2026-02-02T22:49: predicho 2234.91 USD, real 2349.28 USD, error 114.37 USD11Error de predicción medio en 29 predicciones: 83.87103448275862068965517241 USD12Cambio medio por recomendación: 4.787931034482758620689655172 USD13Varianza estándar de los cambios: 104.42 USD14Días rentables: 51.72%15Días con pérdidas: 48.28%Mostrar todoLa 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 probamos23# Obtener muchas cotizaciones4wethusdc_pool = read_pool(WETHUSDC_ADDRESS, True)5wethusdc_quotes = get_quotes(6 wethusdc_pool,7 w3.eth.block_number - CYCLE_BLOCKS*CYCLES_FOR_TEST,8 w3.eth.block_number,9 CYCLE_BLOCKS,10)1112wethwbtc_pool = read_pool(WETHWBTC_ADDRESS)13wethwbtc_quotes = get_quotes(14 wethwbtc_pool,15 w3.eth.block_number - CYCLE_BLOCKS*CYCLES_FOR_TEST,16 w3.eth.block_number,17 CYCLE_BLOCKS18)Mostrar todoMiramos hacia atrás CYCLES_FOR_TEST (especificado como 40 aquí) días.
1# Crear predicciones y verificarlas con el historial real23total_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 sí 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].price3 prediction_time_price = wethusdc_quotes[index+CYCLES_BACK-1].priceObté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 += error3 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_price3 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")23length_changes = Decimal(len(changes))4mean_change = sum(changes, Decimal(0)) / length_changes5print (f"Cambio medio por recomendación: {mean_change} USD")6var = sum((x - mean_change) ** 2 for x in changes) / length_changes7print (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.
-
Instala Foundry (opens in a new tab)
-
Inicia
anvil(opens in a new tab)1anvil --fork-url https://eth.drpc.org --block-time 12anvilestá 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 comandocast(opens in a new tab) que usamos para manipular la cadena de bloques. -
Cuando se ejecuta en
anvil, hay diez cuentas de prueba que tienen ETH: establece las variables de entorno para la primera1PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff802ADDRESS=`cast wallet address $PRIVATE_KEY` -
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=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc22USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f56404SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C058615645WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB486USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 -
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 -
Usa
SwapRouterpara intercambiar 500 WETH por USDC.1cast send $WETH_ADDRESS "approve(address,uint256)" $SWAP_ROUTER 500ether --private-key $PRIVATE_KEY2MAXINT=`cast max-int uint256`3cast send $SWAP_ROUTER \4 "exactInput((bytes,address,uint256,uint256,uint256))" \5 "($WETH_TO_USDC,$ADDRESS,$MAXINT,500ether,1000000)" \6 --private-key $PRIVATE_KEYLa llamada
approvecrea una asignación que permite aSwapRoutergastar algunos de nuestros tokens. Los contratos no pueden supervisar eventos, por lo que si transferimos tokens directamente al contratoSwapRouter, no sabría que se le ha pagado. En cambio, permitimos que el contratoSwapRoutergaste una cierta cantidad, y luegoSwapRouterlo hace. Esto se hace a través de una función llamada porSwapRouter, para que sepa si tuvo éxito. -
Verifica que tienes suficientes de ambos tokens.
1cast call $WETH_ADDRESS "balanceOf(address)" $ADDRESS | cast from-wei2echo `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-trade2uv run agent.pyLa salida será similar a:
1(ai-trading-agent) qbzzt@Ori-Cloudnomics:~/260215-ai-agent$ uv run agent.py2Precio actual: 1843.163En 2026-02-06T23:07, precio esperado: 1724.41 USD4Saldos de la cuenta antes de la operación:5Saldo de USDC: 927301.5782726Saldo de WETH: 5007Vender, espero que el precio baje en 118.75 USD8Transacción de aprobación enviada: 74e367ddbb407c1aaf567d87aa5863049991b1d2aa092b6b85195d925e2bd41f9Transacción de aprobación minada.10Transacción de venta enviada: fad1bcf938585c9e90364b26ac7a80eea9efd34c37e5db81e58d7655bcae28bf11Transacción de venta minada.12Saldos de la cuenta después de la operación:13Saldo de USDC: 929143.79711614Saldo de WETH: 499Mostrar todoPara usarlo realmente, necesitas algunos cambios menores.
- En la línea 14, cambia
MAINNET_URLa un punto de acceso real, comohttps://eth.drpc.org - En la línea 28, cambia
PRIVATE_KEYa 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=1La 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_ABI5)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.")101112def sell():13 approve_token(wethusdc_pool.token1.contract,14 WETH_TRADE_AMOUNT * 10**wethusdc_pool.token1.decimals)15 txn = swap_router.functions.exactInput(SELL_PARAMS).build_transaction(txn_params())16 signed_txn = w3.eth.account.sign_transaction(txn, private_key=PRIVATE_KEY)17 tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction)18 print(f"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 todoLas 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()45 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()34if (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()1011print("Saldos de la cuenta después de la operación:")12balances()Mostrar todoEste 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
WETHsi 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