Перейти к основному содержанию

Создайте своего торгового агента с ИИ на Ethereum

ИИ
торговля
агент
python
Intermediate
Ori Pomerantz
13 февраля 2026 г.
22 минута прочтения

В этом руководстве вы узнаете, как создать простого торгового агента с ИИ. Этот агент работает следующим образом:

  1. Считывание текущих и прошлых цен токена, а также другой потенциально релевантной информации
  2. Создание запроса с этой информацией, а также с фоновой информацией для объяснения ее возможной релевантности
  3. Отправка запроса и получение прогнозируемой цены
  4. Торговля на основе рекомендации
  5. Ожидание и повторение

Этот агент демонстрирует, как считывать информацию, преобразовывать ее в запрос, который дает применимый ответ, и использовать этот ответ. Все это необходимые шаги для агента с ИИ. Этот агент реализован на языке Python, поскольку это самый распространенный язык, используемый в сфере ИИ.

Зачем это делать?

Автоматизированные торговые агенты позволяют разработчикам выбирать и выполнять торговую стратегию. Агенты с ИИ позволяют использовать более сложные и динамичные торговые стратегии, потенциально используя информацию и алгоритмы, которые разработчик даже не рассматривал для использования.

Инструменты

В этом руководстве используется Python (opens in a new tab), библиотека Web3 (opens in a new tab), и Uniswap v3 (opens in a new tab) для получения котировок и торговли.

Почему Python?

Самый широко используемый язык для ИИ — это Python (opens in a new tab), поэтому мы используем его здесь. Не беспокойтесь, если вы не знаете Python. Язык очень понятен, и я объясню, что именно он делает.

Библиотека Web3 (opens in a new tab) является самым распространенным API для Ethereum на языке Python. Она довольно проста в использовании.

Торговля в блокчейне

Существует много распределенных бирж (DEX), которые позволяют торговать токенами на Ethereum. Однако у них, как правило, схожие обменные курсы благодаря арбитражу.

Uniswap (opens in a new tab) — это широко используемая DEX, которую мы можем использовать как для получения котировок (чтобы увидеть относительную стоимость токенов), так и для совершения сделок.

OpenAI

Для работы с большой языковой моделью я решил начать с OpenAI (opens in a new tab). Для запуска приложения из этого руководства вам понадобится оплатить доступ к API. Минимального платежа в 5 $ более чем достаточно.

Пошаговая разработка

Чтобы упростить разработку, мы будем действовать поэтапно. Каждый шаг — это отдельная ветка в GitHub.

Начало работы

Вот шаги для начала работы в UNIX или Linux (включая WSL (opens in a new tab))

  1. Если у вас еще нет, скачайте и установите Python (opens in a new tab).

  2. Клонируйте репозиторий GitHub.

    1git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started
    2cd 260215-ai-agent
  3. Установите uv (opens in a new tab). Команда в вашей системе может отличаться.

    1pipx install uv
  4. Скачайте библиотеки.

    1uv sync
  5. Активируйте виртуальное окружение.

    1source .venv/bin/activate
  6. Чтобы убедиться, что Python и Web3 работают корректно, запустите python3 и передайте ему эту программу. Вы можете ввести ее в строке >>>; нет необходимости создавать файл.

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

Чтение из блокчейна

Следующий шаг — чтение из блокчейна. Для этого вам нужно переключиться на ветку 02-read-quote, а затем использовать uv для запуска программы.

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

Вы должны получить список объектов Quote, каждый из которых содержит временную метку, цену и актив (в настоящее время это всегда WETH/USDC).

Вот пошаговое объяснение.

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
Показать все

Импортируйте необходимые нам библиотеки. Они объясняются ниже, по мере их использования.

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

Заменяет print из Python на версию, которая всегда немедленно сбрасывает вывод. Это полезно в долго работающем скрипте, потому что мы не хотим ждать обновлений статуса или вывода для отладки.

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

URL для доступа к основной сети. Вы можете получить его у сервиса «Узел как услуга» или использовать один из тех, что рекламируются в 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

Блок в основной сети Ethereum обычно создается каждые двенадцать секунд, поэтому это количество блоков, которое мы ожидаем за определенный период времени. Обратите внимание, что это не точное значение. Когда предлагающий блок недоступен, этот блок пропускается, и время до следующего блока составляет 24 секунды. Если бы мы хотели получить точный блок по временной метке, мы бы использовали двоичный поиск (opens in a new tab). Однако для наших целей этого достаточно. Предсказание будущего — не точная наука.

1CYCLE_BLOCKS = DAY_BLOCKS

Размер цикла. Мы просматриваем котировки один раз за цикл и пытаемся оценить стоимость в конце следующего цикла.

1# Адрес пула, который мы считываем
2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")

Значения котировок берутся из пула Uniswap 3 USDC/WETH по адресу 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 (opens in a new tab). Этот адрес уже в формате с контрольной суммой, но лучше использовать Web3.to_checksum_address (opens in a new tab), чтобы сделать код повторно используемым.

1POOL_ABI = [
2 { "name": "slot0", ... },
3 { "name": "token0", ... },
4 { "name": "token1", ... },
5]
6
7ERC20_ABI = [
8 { "name": "symbol", ... },
9 { "name": "decimals", ... }
10]
Показать все

Это ABI (opens in a new tab) для двух контрактов, с которыми нам нужно взаимодействовать. Для краткости кода мы включаем только те функции, которые нам нужно вызывать.

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

Инициализируйте библиотеку Web3 (opens in a new tab) и подключитесь к узлу Ethereum.

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

Это один из способов создания класса данных в Python. Тип данных Contract (opens in a new tab) используется для подключения к контракту. Обратите внимание на (frozen=True). В Python логические значения (opens in a new tab) определяются как True или False, с заглавной буквы. Этот класс данных является frozen, что означает, что поля не могут быть изменены.

Обратите внимание на отступы. В отличие от C-подобных языков (opens in a new tab), Python использует отступы для обозначения блоков. Интерпретатор Python знает, что следующее определение не является частью этого класса данных, потому что оно не начинается с тем же отступом, что и поля класса данных.

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

Тип Decimal (opens in a new tab) используется для точной обработки десятичных дробей.

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

Так определяется функция в Python. Определение имеет отступ, чтобы показать, что оно по-прежнему является частью PoolInfo.

В функции, которая является частью класса данных, первым параметром всегда является self — экземпляр класса данных, который ее вызвал. Здесь есть еще один параметр — номер блока.

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

Если бы мы могли читать будущее, нам бы не нужен был ИИ для торговли.

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

Синтаксис для вызова функции в EVM из Web3 следующий: <contract object>.functions.<function name>().call(<parameters>). Параметрами могут быть параметры функции EVM (если они есть; здесь их нет) или именованные параметры (opens in a new tab) для изменения поведения блокчейна. Здесь мы используем один, block_identifier, чтобы указать номер блока, в котором мы хотим выполнить операцию.

Результатом является эта структура в виде массива (opens in a new tab). Первое значение — это функция обменного курса между двумя токенами.

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

Для сокращения вычислений в сети Uniswap v3 хранит не фактический коэффициент обмена, а его квадратный корень. Поскольку EVM не поддерживает математику с плавающей запятой или дроби, вместо фактического значения ответ будет price&#x22C5296

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

Необработанная цена, которую мы получаем, — это количество token0, которое мы получаем за каждый token1. В нашем пуле token0 — это USDC (стабильная монета с такой же стоимостью, как у доллара США), а token1 — это WETH (opens in a new tab). Значение, которое нам действительно нужно, — это количество долларов за WETH, а не обратное значение.

Десятичный множитель — это соотношение между десятичными множителями (opens in a new tab) для двух токенов.

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

Этот класс данных представляет собой котировку: цену определенного актива в данный момент времени. На данный момент поле asset не имеет значения, поскольку мы используем один пул и, следовательно, имеем один актив. Однако позже мы добавим больше активов.

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 )
Показать все

Эта функция принимает адрес и возвращает информацию о контракте токена по этому адресу. Чтобы создать новый Contract Web3 (opens in a new tab), мы передаем адрес и ABI в 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 )
Показать все

Эта функция возвращает все, что нам нужно о конкретном пуле (opens in a new tab). Синтаксис f"<string>" — это форматированная строка (opens in a new tab).

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

Получить объект Quote. Значение по умолчанию для block_numberNone (нет значения).

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

Если номер блока не был указан, используется w3.eth.block_number, который является последним номером блока. Это синтаксис для оператора if (opens in a new tab).

Может показаться, что было бы лучше просто установить значение по умолчанию w3.eth.block_number, но это не очень хорошо работает, потому что это был бы номер блока на момент определения функции. В долго работающем агенте это стало бы проблемой.

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 )

Используйте библиотеку datetime (opens in a new tab), чтобы отформатировать ее в формат, читаемый для людей и больших языковых моделей (LLM). Используйте Decimal.quantize (opens in a new tab), чтобы округлить значение до двух десятичных знаков.

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

В Python вы определяете список (opens in a new tab), который может содержать только определенный тип, используя list[<type>].

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

В Python цикл for (opens in a new tab) обычно итерируется по списку. Список номеров блоков для поиска котировок получается из range (opens in a new tab).

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

Для каждого номера блока получите объект Quote и добавьте его в список quotes. Затем верните этот список.

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)
Показать все

Это основной код скрипта. Прочитайте информацию о пуле, получите двенадцать котировок и pprint (opens in a new tab) их.

Создание подсказки

Далее нам нужно преобразовать этот список котировок в подсказку для LLM и получить ожидаемое будущее значение.

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

Результатом будет подсказка для LLM, подобная этой:

1Given these quotes:
2Asset: WETH/USDC
3 2026-01-20T16:34 3016.21
4 .
5 .
6 .
7 2026-02-01T17:49 2299.10
8
9Asset: WBTC/WETH
10 2026-01-20T16:34 29.84
11 .
12 .
13 .
14 2026-02-01T17:50 33.46
15
16
17What would you expect the value for WETH/USDC to be at time 2026-02-02T17:56?
18
19Provide your answer as a single number rounded to two decimal places,
20without any other text.
Показать все

Обратите внимание, что здесь есть котировки для двух активов, WETH/USDC и WBTC/WETH. Добавление котировок из другого актива может повысить точность прогноза.

Как выглядит подсказка

Эта подсказка содержит три раздела, которые довольно часто встречаются в подсказках для LLM.

  1. Информация. LLM обладают большим объемом информации, полученной в ходе обучения, но обычно у них нет самой последней. Именно по этой причине нам нужно получить здесь последние котировки. Добавление информации в подсказку называется поисково-дополненной генерацией (RAG) (opens in a new tab).

  2. Собственно вопрос. Это то, что мы хотим знать.

  3. Инструкции по форматированию вывода. Обычно LLM дает нам оценку с объяснением, как она была получена. Это лучше для людей, но компьютерной программе нужна только суть.

Объяснение кода

Вот новый код.

1from datetime import datetime, timezone, timedelta

Нам нужно предоставить LLM время, для которого мы хотим получить оценку. Чтобы получить время «n минут/часов/дней» в будущем, мы используем класс timedelta (opens in a new tab).

1# Адреса пулов, которые мы считываем
2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")
3WETHWBTC_ADDRESS = Web3.to_checksum_address("0xCBCdF9626bC03E24f779434178A73a0B4bad62eD")

У нас есть два пула, которые нам нужно прочитать.

1@dataclass(frozen=True)
2class PoolInfo:
3 .
4 .
5 .
6 reverse: bool = False
7
8 def get_price(self, block: int) -> Decimal:
9 assert block <= w3.eth.block_number, "Block is in the future"
10 sqrt_price_x96 = Decimal(self.contract.functions.slot0().call(block_identifier=block)[0])
11 raw_price = (sqrt_price_x96 / Decimal(2**96)) ** 2 # (token1 per token0)
12 if self.reverse:
13 return 1/(raw_price * self.decimal_factor)
14 else:
15 return raw_price * self.decimal_factor
Показать все

В пуле WETH/USDC мы хотим знать, сколько token0 (USDC) нам нужно, чтобы купить один token1 (WETH). В пуле WETH/WBTC мы хотим знать, сколько token1 (WETH) нам нужно, чтобы купить один token0 (WBTC, что является обернутым Bitcoin). Нам нужно отслеживать, нужно ли инвертировать соотношение пула.

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 )
Показать все

Чтобы знать, нужно ли инвертировать пул, мы должны получить это как входные данные для read_pool. Кроме того, символ актива должен быть настроен правильно.

Синтаксис <a> if <b> else <c> является эквивалентом Python тернарного условного оператора (opens in a new tab), который в C-подобном языке был бы <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

Эта функция строит строку, которая форматирует список объектов Quote, предполагая, что все они относятся к одному и тому же активу.

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

В Python многострочные строковые литералы (opens in a new tab) пишутся как """ .... """.

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

Здесь мы используем шаблон MapReduce (opens in a new tab) для генерации строки для каждого списка котировок с помощью format_quotes, а затем объединяем их в одну строку для использования в подсказке.

1What would you expect the value for {asset} to be at time {expected_time}?
2
3Provide your answer as a single number rounded to two decimal places,
4without any other text.
5 """

Остальная часть подсказки соответствует ожиданиям.

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)
Показать все

Просмотрите два пула и получите котировки из обоих.

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

Определите будущий момент времени, для которого мы хотим получить оценку, и создайте подсказку.

Взаимодействие с LLM

Далее мы запрашиваем реальную LLM и получаем ожидаемое будущее значение. Я написал эту программу с использованием OpenAI, поэтому, если вы хотите использовать другого провайдера, вам нужно будет ее скорректировать.

  1. Получите аккаунт OpenAI (opens in a new tab)

  2. Пополните счет (opens in a new tab) — минимальная сумма на момент написания составляет 5 долларов

  3. Создайте ключ API (opens in a new tab)

  4. В командной строке экспортируйте ключ API, чтобы ваша программа могла его использовать

    1export OPENAI_API_KEY=sk-<the rest of the key goes here>
  5. Получите и запустите агент

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

Вот новый код.

1from openai import OpenAI
2
3open_ai = OpenAI() # Клиент считывает переменную окружения OPENAI_API_KEY

Импортируйте и создайте экземпляр API OpenAI.

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

Вызовите API OpenAI (open_ai.chat.completions.create) для создания ответа.

1expected_price = Decimal(response.choices[0].message.content.strip())
2current_price = wethusdc_quotes[-1].price
3
4print ("Current price:", wethusdc_quotes[-1].price)
5print(f"In {future_time}, expected price: {expected_price} USD")
6
7if (expected_price > current_price):
8 print(f"Buy, I expect the price to go up by {expected_price - current_price} USD")
9else:
10 print(f"Sell, I expect the price to go down by {current_price - expected_price} USD")
Показать все

Выведите цену и дайте рекомендацию на покупку или продажу.

Тестирование прогнозов

Теперь, когда мы можем генерировать прогнозы, мы также можем использовать исторические данные для оценки, создаем ли мы полезные прогнозы.

1uv run test-predictor.py

Ожидаемый результат подобен этому:

1Prediction for 2026-01-05T19:50: predicted 3138.93 USD, real 3218.92 USD, error 79.99 USD
2Prediction for 2026-01-06T19:56: predicted 3243.39 USD, real 3221.08 USD, error 22.31 USD
3Prediction for 2026-01-07T20:02: predicted 3223.24 USD, real 3146.89 USD, error 76.35 USD
4Prediction for 2026-01-08T20:11: predicted 3150.47 USD, real 3092.04 USD, error 58.43 USD
5.
6.
7.
8Prediction for 2026-01-31T22:33: predicted 2637.73 USD, real 2417.77 USD, error 219.96 USD
9Prediction for 2026-02-01T22:41: predicted 2381.70 USD, real 2318.84 USD, error 62.86 USD
10Prediction for 2026-02-02T22:49: predicted 2234.91 USD, real 2349.28 USD, error 114.37 USD
11Mean prediction error over 29 predictions: 83.87103448275862068965517241 USD
12Mean change per recommendation: 4.787931034482758620689655172 USD
13Standard variance of changes: 104.42 USD
14Profitable days: 51.72%
15Losing days: 48.28%
Показать все

Большая часть тестера идентична агенту, но вот новые или измененные части.

1CYCLES_FOR_TEST = 40 # Для бэктеста, сколько циклов мы тестируем
2
3# Получить много котировок
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)
Показать все

Мы смотрим на CYCLES_FOR_TEST (здесь указано 40) дней назад.

1# Создать прогнозы и проверить их на реальной истории
2
3total_error = Decimal(0)
4changes = []

Нас интересуют два типа ошибок. Первая, total_error, — это просто сумма ошибок, допущенных предсказателем.

Чтобы понять вторую, changes, нам нужно вспомнить о цели агента. Его цель не предсказать соотношение WETH/USDC (цену ETH). Его цель — выдавать рекомендации о продаже и покупке. Если текущая цена составляет 2000 $, и он предсказывает 2010 $ на завтра, мы не возражаем, если фактический результат будет 2020 $, и мы заработаем дополнительные деньги. Но мы возражаем, если он предсказал 2010 $, и купил ETH на основе этой рекомендации, а цена упала до 1990 $.

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

Мы можем рассматривать только те случаи, когда доступна полная история (значения, используемые для прогноза, и реальное значение для сравнения). Это означает, что самый новый случай должен был начаться CYCLES_BACK назад.

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

Используйте срезы (opens in a new tab), чтобы получить то же количество образцов, которое использует агент. Код между этим и следующим сегментом — это тот же код для получения прогноза, который есть в агенте.

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

Получите предсказанную цену, реальную цену и цену на момент прогноза. Нам нужна цена на момент прогноза, чтобы определить, была ли рекомендация купить или продать.

1 error = abs(predicted_price - real_price)
2 total_error += error
3 print (f"Prediction for {prediction_time}: predicted {predicted_price} USD, real {real_price} USD, error {error} USD")

Вычислите ошибку и добавьте ее к общей сумме.

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)

Для changes мы хотим получить денежное влияние от покупки или продажи одного ETH. Поэтому сначала нам нужно определить рекомендацию, затем оценить, как изменилась фактическая цена, и принесла ли рекомендация деньги (положительное изменение) или стоила денег (отрицательное изменение).

1print (f"Mean prediction error over {len(wethusdc_quotes)-CYCLES_BACK} predictions: {total_error / Decimal(len(wethusdc_quotes)-CYCLES_BACK)} USD")
2
3length_changes = Decimal(len(changes))
4mean_change = sum(changes, Decimal(0)) / length_changes
5print (f"Mean change per recommendation: {mean_change} USD")
6var = sum((x - mean_change) ** 2 for x in changes) / length_changes
7print (f"Standard variance of changes: {var.sqrt().quantize(Decimal("0.01"))} USD")

Сообщите о результатах.

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

Используйте filter (opens in a new tab), чтобы подсчитать количество прибыльных дней и количество убыточных дней. Результатом является объект-фильтр, который нам нужно преобразовать в список, чтобы получить его длину.

Отправка транзакций

Теперь нам нужно фактически отправить транзакции. Однако я не хочу тратить реальные деньги на этом этапе, пока система не доказала свою эффективность. Вместо этого мы создадим локальный форк основной сети и будем «торговать» в этой сети.

Вот шаги по созданию локального форка и включению торговли.

  1. Установите Foundry (opens in a new tab)

  2. Запустите anvil (opens in a new tab)

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

    anvil слушает на URL по умолчанию для Foundry, http://localhost:8545 (opens in a new tab), поэтому нам не нужно указывать URL для команды cast (opens in a new tab), которую мы используем для манипулирования блокчейном.

  3. При работе в anvil есть десять тестовых аккаунтов с ETH — установите переменные окружения для первого

    1PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    2ADDRESS=`cast wallet address $PRIVATE_KEY`
  4. Это контракты, которые нам нужно использовать. SwapRouter (opens in a new tab) — это контракт Uniswap v3, который мы используем для фактической торговли. Мы могли бы торговать напрямую через пул, но так гораздо проще.

    Две нижние переменные — это пути Uniswap v3, необходимые для обмена между WETH и USDC.

    1WETH_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
    2USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    3POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640
    4SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C05861564
    5WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    6USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
  5. На каждом из тестовых аккаунтов есть 10 000 ETH. Используйте контракт WETH, чтобы обернуть 1000 ETH и получить 1000 WETH для торговли.

    1cast send $WETH_ADDRESS "deposit()" --value 1000ether --private-key $PRIVATE_KEY
  6. Используйте SwapRouter, чтобы обменять 500 WETH на 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

    Вызов approve создает разрешение, которое позволяет SwapRouter тратить некоторые из наших токенов. Контракты не могут отслеживать события, поэтому, если мы переведем токены непосредственно на контракт SwapRouter, он не узнает, что ему заплатили. Вместо этого мы разрешаем контракту SwapRouter потратить определенную сумму, а затем SwapRouter делает это. Это делается через функцию, вызываемую SwapRouter, поэтому он знает, была ли она успешной.

  7. Убедитесь, что у вас достаточно обоих токенов.

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

Теперь, когда у нас есть WETH и USDC, мы можем запустить агент.

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

Вывод будет выглядеть примерно так:

1(ai-trading-agent) qbzzt@Ori-Cloudnomics:~/260215-ai-agent$ uv run agent.py
2Current price: 1843.16
3In 2026-02-06T23:07, expected price: 1724.41 USD
4Account balances before trade:
5USDC Balance: 927301.578272
6WETH Balance: 500
7Sell, I expect the price to go down by 118.75 USD
8Approve transaction sent: 74e367ddbb407c1aaf567d87aa5863049991b1d2aa092b6b85195d925e2bd41f
9Approve transaction mined.
10Sell transaction sent: fad1bcf938585c9e90364b26ac7a80eea9efd34c37e5db81e58d7655bcae28bf
11Sell transaction mined.
12Account balances after trade:
13USDC Balance: 929143.797116
14WETH Balance: 499
Показать все

Чтобы использовать его на самом деле, вам понадобятся несколько незначительных изменений.

  • В строке 14 измените MAINNET_URL на реальную точку доступа, например https://eth.drpc.org
  • В строке 28 измените PRIVATE_KEY на ваш собственный приватный ключ
  • Если вы не очень богаты и не можете покупать или продавать 1 ETH каждый день для непроверенного агента, вы можете изменить строку 29, чтобы уменьшить WETH_TRADE_AMOUNT

Объяснение кода

Вот новый код.

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

Те же переменные, что мы использовали на шаге 4.

1WETH_TRADE_AMOUNT=1

Сумма для торговли.

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

Для фактической торговли нам нужна функция approve. Мы также хотим показать балансы до и после, поэтому нам также нужна balanceOf.

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

В ABI SwapRouter нам нужен только exactInput. Есть связанная функция, exactOutput, которую мы могли бы использовать для покупки ровно одного WETH, но для простоты мы просто используем exactInput в обоих случаях.

1account = w3.eth.account.from_key(PRIVATE_KEY)
2swap_router = w3.eth.contract(
3 address=SWAP_ROUTER_ADDRESS,
4 abi=SWAP_ROUTER_ABI
5)

Определения Web3 для account (opens in a new tab) и контракта 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 }

Параметры транзакции. Нам нужна здесь функция, потому что nonce (opens in a new tab) должен меняться каждый раз.

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

Утвердите разрешение на токены для 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)

Вот как мы отправляем транзакцию в Web3. Сначала мы используем объект Contract (opens in a new tab) для создания транзакции. Затем мы используем web3.eth.account.sign_transaction (opens in a new tab) для подписи транзакции, используя PRIVATE_KEY. Наконец, мы используем w3.eth.send_raw_transaction (opens in a new tab) для отправки транзакции.

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

w3.eth.wait_for_transaction_receipt (opens in a new tab) ожидает, пока транзакция будет включена в блок. При необходимости возвращается квитанция.

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}

Это параметры при продаже 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 }

В отличие от SELL_PARAMS, параметры покупки могут меняться. Входная сумма — это стоимость 1 WETH, доступная в quote.

1def buy(quote: Quote):
2 buy_params = make_buy_params(quote)
3 approve_token(wethusdc_pool.token0.contract, buy_params["amountIn"])
4 txn = swap_router.functions.exactInput(buy_params).build_transaction(txn_params())
5 signed_txn = w3.eth.account.sign_transaction(txn, private_key=PRIVATE_KEY)
6 tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction)
7 print(f"Buy transaction sent: {tx_hash.hex()}")
8 w3.eth.wait_for_transaction_receipt(tx_hash)
9 print("Buy transaction mined.")
10
11
12def sell():
13 approve_token(wethusdc_pool.token1.contract,
14 WETH_TRADE_AMOUNT * 10**wethusdc_pool.token1.decimals)
15 txn = swap_router.functions.exactInput(SELL_PARAMS).build_transaction(txn_params())
16 signed_txn = w3.eth.account.sign_transaction(txn, private_key=PRIVATE_KEY)
17 tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction)
18 print(f"Sell transaction sent: {tx_hash.hex()}")
19 w3.eth.wait_for_transaction_receipt(tx_hash)
20 print("Sell transaction mined.")
Показать все

Функции buy() и sell() почти идентичны. Сначала мы утверждаем достаточное разрешение для SwapRouter, а затем вызываем его с правильным путем и суммой.

1def balances():
2 token0_balance = wethusdc_pool.token0.contract.functions.balanceOf(account.address).call()
3 token1_balance = wethusdc_pool.token1.contract.functions.balanceOf(account.address).call()
4
5 print(f"{wethusdc_pool.token0.symbol} Balance: {Decimal(token0_balance) / Decimal(10 ** wethusdc_pool.token0.decimals)}")
6 print(f"{wethusdc_pool.token1.symbol} Balance: {Decimal(token1_balance) / Decimal(10 ** wethusdc_pool.token1.decimals)}")

Сообщите о балансах пользователя в обеих валютах.

1print("Account balances before trade:")
2balances()
3
4if (expected_price > current_price):
5 print(f"Buy, I expect the price to go up by {expected_price - current_price} USD")
6 buy(wethusdc_quotes[-1])
7else:
8 print(f"Sell, I expect the price to go down by {current_price - expected_price} USD")
9 sell()
10
11print("Account balances after trade:")
12balances()
Показать все

Этот агент в настоящее время работает только один раз. Однако вы можете изменить его для непрерывной работы либо запустив его из crontab (opens in a new tab), либо обернув строки 368-400 в цикл и используя time.sleep (opens in a new tab), чтобы дождаться времени для следующего цикла.

Возможные улучшения

Это не полная производственная версия; это лишь пример для обучения основам. Вот несколько идей для улучшений.

Более умная торговля

Есть два важных факта, которые агент игнорирует при принятии решения о действиях.

  • Величина ожидаемого изменения. Агент продает фиксированное количество WETH, если ожидается снижение цены, независимо от величины этого снижения. Вероятно, было бы лучше игнорировать незначительные изменения и продавать в зависимости от того, на сколько мы ожидаем снижения цены.
  • Текущий портфель. Если 10% вашего портфеля находятся в WETH, и вы думаете, что цена вырастет, вероятно, имеет смысл купить больше. Но если 90% вашего портфеля находятся в WETH, вы можете быть достаточно подвержены риску, и нет необходимости покупать больше. Обратное верно, если вы ожидаете снижения цены.

Что, если вы хотите сохранить свою торговую стратегию в секрете?

Поставщики ИИ могут видеть запросы, которые вы отправляете их LLM, что может раскрыть гениальную торговую систему, разработанную вами с помощью вашего агента. Торговая система, которую использует слишком много людей, бесполезна, потому что слишком много людей пытаются купить, когда вы хотите купить (и цена растет), и пытаются продать, когда вы хотите продать (и цена падает).

Вы можете запустить LLM локально, например, используя LM-Studio (opens in a new tab), чтобы избежать этой проблемы.

От бота с ИИ к агенту с ИИ

Можно с полным основанием утверждать, что это бот с ИИ, а не агент с ИИ. Он реализует относительно простую стратегию, которая опирается на предопределенную информацию. Мы можем включить самосовершенствование, например, предоставив список пулов Uniswap v3 и их последние значения и спросив, какая комбинация имеет наилучшую прогностическую ценность.

Защита от проскальзывания

В настоящее время нет защиты от проскальзывания (opens in a new tab). Если текущая котировка составляет 2000 $, а ожидаемая цена — 2100 $, агент купит. Однако, если до того, как агент купит, стоимость поднимется до 2200 $, покупать больше не имеет смысла.

Чтобы реализовать защиту от проскальзывания, укажите значение amountOutMinimum в строках 325 и 334 файла agent.py (opens in a new tab).

Заключение

Надеюсь, теперь вы знаете достаточно, чтобы начать работать с агентами ИИ. Это не исчерпывающий обзор предмета; этому посвящены целые книги, но этого достаточно, чтобы вы могли начать. Удачи!

Больше моих работ смотрите здесь (opens in a new tab).

Последнее обновление страницы: 10 февраля 2026 г.

Было ли это руководство полезным?