Przejdź do głównej zawartości

Stwórz własnego agenta handlowego AI na Ethereum

AI
handel
agent
python
Średnio zaawansowany
Ori Pomerantz
13 lutego 2026
22 minuta czytania

W tym samouczku dowiesz się, jak zbudować prostego agenta handlowego AI. Agent ten działa w oparciu o następujące kroki:

  1. Odczytanie bieżących i przeszłych cen tokena, a także innych potencjalnie istotnych informacji
  2. Zbudowanie zapytania z tymi informacjami, wraz z informacjami ogólnymi, aby wyjaśnić, w jaki sposób mogą być one istotne
  3. Przesłanie zapytania i otrzymanie prognozowanej ceny
  4. Handel w oparciu o rekomendację
  5. Oczekiwanie i powtórzenie

Ten agent pokazuje, jak odczytywać informacje, przekształcać je w zapytanie, które daje użyteczną odpowiedź i jak z tej odpowiedzi korzystać. Wszystkie te kroki są wymagane w przypadku agenta AI. Agent ten jest zaimplementowany w Pythonie, ponieważ jest to najpopularniejszy język używany w AI.

Po co to robić?

Zautomatyzowani agenci handlowi pozwalają deweloperom wybierać i realizować strategię handlową. Agenci AI pozwalają na bardziej złożone i dynamiczne strategie handlowe, potencjalnie wykorzystując informacje i algorytmy, których użycia deweloper nawet nie brał pod uwagę.

Narzędzia

W tym samouczku wykorzystano Python (opens in a new tab), bibliotekę Web3 (opens in a new tab) oraz Uniswap v3 (opens in a new tab) do notowań i handlu.

Dlaczego Python?

Najczęściej używanym językiem w dziedzinie AI jest Python (opens in a new tab), więc używamy go tutaj. Nie martw się, jeśli nie znasz Pythona. Język jest bardzo przejrzysty, a ja dokładnie wyjaśnię, co robi.

Biblioteka Web3 (opens in a new tab) jest najpopularniejszym API Pythona dla Ethereum. Jest dość łatwa w użyciu.

Handel na blockchainie

Istnieje wiele zdecentralizowanych giełd (DEX), które pozwalają na handel tokenami na Ethereum. Jednakże, ze względu na arbitraż, mają one zazwyczaj podobne kursy wymiany.

Uniswap (opens in a new tab) to powszechnie używana giełda DEX, której możemy używać zarówno do notowań (w celu sprawdzenia względnych wartości tokenów), jak i transakcji.

OpenAI

Jeśli chodzi o duży model językowy, zdecydowałem się zacząć od OpenAI (opens in a new tab). Aby uruchomić aplikację z tego samouczka, trzeba będzie zapłacić za dostęp do API. Minimalna opłata w wysokości 5 USD jest więcej niż wystarczająca.

Programowanie, krok po kroku

Aby uprościć programowanie, postępujemy etapami. Każdy krok to gałąź w GitHub.

Pierwsze kroki

Oto kroki, aby rozpocząć pracę w systemie UNIX lub Linux (w tym WSL (opens in a new tab))

  1. Jeśli jeszcze go nie masz, pobierz i zainstaluj Pythona (opens in a new tab).

  2. Sklonuj repozytorium GitHub.

    1git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started
    2cd 260215-ai-agent
  3. Zainstaluj uv (opens in a new tab). Polecenie w twoim systemie może być inne.

    1pipx install uv
  4. Pobierz biblioteki.

    1uv sync
  5. Aktywuj środowisko wirtualne.

    1source .venv/bin/activate
  6. Aby sprawdzić, czy Python i Web3 działają poprawnie, uruchom python3 i podaj mu ten program. Można go wpisać w wierszu poleceń >>>; nie ma potrzeby tworzenia pliku.

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

Odczyt z blockchaina

Następnym krokiem jest odczyt z blockchaina. Aby to zrobić, należy przełączyć się na gałąź 02-read-quote, a następnie użyć uv do uruchomienia programu.

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

Powinna zostać wyświetlona lista obiektów Quote, z których każdy ma znacznik czasu, cenę i aktywo (obecnie zawsze WETH/USDC).

Oto wyjaśnienie linijka po linijce.

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
Pokaż wszystko

Zaimportuj biblioteki, których potrzebujemy. Są one wyjaśnione poniżej, gdy są używane.

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

Zastępuje funkcję print Pythona wersją, która zawsze natychmiast opróżnia bufor wyjścia. Jest to przydatne w długo działającym skrypcie, ponieważ nie chcemy czekać na aktualizacje statusu lub dane wyjściowe debugowania.

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

Adres URL do sieci głównej. Możesz uzyskać go z węzła jako usługa lub użyć jednego z tych reklamowanych w 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

Blok w sieci głównej Ethereum pojawia się zazwyczaj co dwanaście sekund, więc są to liczby bloków, których spodziewamy się w danym okresie. Należy pamiętać, że nie jest to dokładna liczba. Gdy proposer bloku nie działa, ten blok jest pomijany, a czas do następnego bloku wynosi 24 sekundy. Gdybyśmy chcieli uzyskać dokładny blok dla znacznika czasu, użylibyśmy wyszukiwania binarnego (opens in a new tab). Jednak na nasze potrzeby jest to wystarczająco dokładne. Przewidywanie przyszłości nie jest nauką ścisłą.

1CYCLE_BLOCKS = DAY_BLOCKS

Rozmiar cyklu. Przeglądamy notowania raz na cykl i próbujemy oszacować wartość na koniec następnego cyklu.

1# Adres puli, z której odczytujemy
2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")

Wartości notowań są pobierane z puli Uniswap 3 USDC/WETH pod adresem 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 (opens in a new tab). Ten adres jest już w formie sumy kontrolnej, ale lepiej jest użyć Web3.to_checksum_address (opens in a new tab), aby kod był wielokrotnego użytku.

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

Są to ABI (opens in a new tab) dla dwóch kontraktów, z którymi musimy się skontaktować. Aby kod był zwięzły, dołączamy tylko te funkcje, które musimy wywołać.

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

Zainicjuj bibliotekę Web3 (opens in a new tab) i połącz się z węzłem Ethereum.

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

Jest to jeden ze sposobów tworzenia klasy danych w Pythonie. Typ danych Contract (opens in a new tab) jest używany do łączenia się z kontraktem. Zwróć uwagę na (frozen=True). W Pythonie wartości logiczne (opens in a new tab) są zdefiniowane jako True lub False, pisane wielką literą. Ta klasa danych jest frozen, co oznacza, że jej pól nie można modyfikować.

Zwróć uwagę na wcięcie. W przeciwieństwie do języków wywodzących się z C (opens in a new tab), Python używa wcięć do oznaczania bloków. Interpreter Pythona wie, że poniższa definicja nie jest częścią tej klasy danych, ponieważ nie zaczyna się na tym samym poziomie wcięcia, co pola klasy danych.

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

Typ Decimal (opens in a new tab) służy do dokładnej obsługi ułamków dziesiętnych.

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

W ten sposób definiuje się funkcję w Pythonie. Definicja jest wcięta, aby pokazać, że nadal jest częścią PoolInfo.

W funkcji, która jest częścią klasy danych, pierwszym parametrem jest zawsze self, czyli instancja klasy danych, która została tutaj wywołana. Jest tu jeszcze jeden parametr, numer bloku.

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

Gdybyśmy potrafili czytać przyszłość, nie potrzebowalibyśmy AI do handlu.

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

Składnia wywoływania funkcji na EVM z Web3 jest następująca: <obiekt kontraktu>.functions.<nazwa funkcji>"().call(<parametry>). Parametry mogą być parametrami funkcji EVM (jeśli istnieją; tutaj ich nie ma) lub parametrami nazwanymi (opens in a new tab) do modyfikowania zachowania blockchaina. Tutaj używamy jednego, block_identifier, aby określić numer bloku, w którym chcemy uruchomić.

Wynikiem jest ta struktura, w formie tablicy (opens in a new tab). Pierwsza wartość jest funkcją kursu wymiany między dwoma tokenami.

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

Aby zmniejszyć liczbę obliczeń on-chain, Uniswap v3 nie przechowuje rzeczywistego współczynnika wymiany, a raczej jego pierwiastek kwadratowy. Ponieważ EVM nie obsługuje matematyki zmiennoprzecinkowej ani ułamków, zamiast rzeczywistej wartości, odpowiedzią jest price&#x22C5296

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

Cena surowa, którą otrzymujemy, to liczba token0, którą otrzymujemy za każdy token1. W naszej puli token0 to USDC (stablecoin o tej samej wartości co dolar amerykański), a token1 to WETH (opens in a new tab). Wartością, której tak naprawdę potrzebujemy, jest liczba dolarów za WETH, a nie odwrotność.

Współczynnik dziesiętny to stosunek między współczynnikami dziesiętnymi (opens in a new tab) dla dwóch tokenów.

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

Ta klasa danych reprezentuje notowanie: cenę określonego aktywa w danym momencie. W tym momencie pole asset jest nieistotne, ponieważ używamy jednej puli, a zatem mamy jedno aktywo. Jednak później dodamy więcej aktywów.

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 )
Pokaż wszystko

Funkcja ta pobiera adres i zwraca informacje o kontrakcie tokena pod tym adresem. Aby utworzyć nowy Web3 Contract (opens in a new tab), podajemy adres i ABI do 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 )
Pokaż wszystko

Ta funkcja zwraca wszystko, czego potrzebujemy na temat określonej puli (opens in a new tab). Składnia f"<ciąg znaków>" to sformatowany ciąg znaków (opens in a new tab).

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

Pobierz obiekt Quote. Domyślna wartość dla block_number to None (brak wartości).

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

Jeśli numer bloku nie został określony, użyj w3.eth.block_number, który jest najnowszym numerem bloku. Jest to składnia dla instrukcji if (opens in a new tab).

Może się wydawać, że lepiej byłoby po prostu ustawić wartość domyślną na w3.eth.block_number, ale to nie działa dobrze, ponieważ byłby to numer bloku w momencie definiowania funkcji. W przypadku długo działającego agenta byłby to problem.

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 )

Użyj biblioteki datetime (opens in a new tab), aby sformatować go do formatu czytelnego dla ludzi i dużych modeli językowych (LLM). Użyj Decimal.quantize (opens in a new tab), aby zaokrąglić wartość do dwóch miejsc po przecinku.

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

W Pythonie definiuje się listę (opens in a new tab), która może zawierać tylko określony typ za pomocą list[<type>].

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

W Pythonie pętla for (opens in a new tab) zwykle iteruje po liście. Lista numerów bloków do znalezienia notowań pochodzi z range (opens in a new tab).

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

Dla każdego numeru bloku pobierz obiekt Quote i dołącz go do listy quotes. Następnie zwróć tę listę.

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)
Pokaż wszystko

To jest główny kod skryptu. Odczytaj informacje o puli, uzyskaj dwanaście notowań i wydrukuj je za pomocą pprint (opens in a new tab).

Tworzenie monitu

Następnie musimy przekonwertować tę listę notowań na monit dla LLM i uzyskać oczekiwaną przyszłą wartość.

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

Dane wyjściowe będą teraz monitem dla LLM, podobnym do:

1Biorąc pod uwagę te notowania:
2Aktywa: WETH/USDC
3 2026-01-20T16:34 3016,21
4 .
5 .
6 .
7 2026-02-01T17:49 2299,10
8
9Aktywa: WBTC/WETH
10 2026-01-20T16:34 29,84
11 .
12 .
13 .
14 2026-02-01T17:50 33,46
15
16
17Jakiej wartości WETH/USDC spodziewałbyś się o godzinie 2026-02-02T17:56?
18
19Podaj odpowiedź jako pojedynczą liczbę zaokrągloną do dwóch miejsc po przecinku,
20bez żadnego innego tekstu.
Pokaż wszystko

Zauważ, że są tu notowania dla dwóch aktywów, WETH/USDC i WBTC/WETH. Dodanie notowań z innego aktywa może poprawić dokładność prognozy.

Jak wygląda monit

Ten monit zawiera trzy sekcje, które są dość powszechne w monitach LLM.

  1. Informacje. Modele LLM mają wiele informacji ze swojego szkolenia, ale zazwyczaj nie mają najnowszych. Z tego powodu musimy pobrać tutaj najnowsze notowania. Dodawanie informacji do monitu nazywa się generowaniem rozszerzonym o wyszukiwanie (RAG) (opens in a new tab).

  2. Właściwe pytanie. To jest to, co chcemy wiedzieć.

  3. Instrukcje formatowania danych wyjściowych. Zwykle LLM poda nam szacunkową wartość wraz z wyjaśnieniem, w jaki sposób do niej doszedł. Jest to lepsze dla ludzi, ale program komputerowy potrzebuje tylko wyniku końcowego.

Wyjaśnienie kodu

Oto nowy kod.

1from datetime import datetime, timezone, timedelta

Musimy podać LLM czas, dla którego chcemy uzyskać oszacowanie. Aby uzyskać czas „n minut/godzin/dni” w przyszłości, używamy klasy timedelta (opens in a new tab).

1# Adresy pul, z których odczytujemy
2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")
3WETHWBTC_ADDRESS = Web3.to_checksum_address("0xCBCdF9626bC03E24f779434178A73a0B4bad62eD")

Mamy dwie pule, które musimy odczytać.

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
Pokaż wszystko

W puli WETH/USDC chcemy wiedzieć, ile token0 (USDC) potrzebujemy, aby kupić jeden token1 (WETH). W puli WETH/WBTC chcemy wiedzieć, ile token1 (WETH) potrzebujemy, aby kupić jeden token0 (WBTC, czyli opakowany Bitcoin). Musimy śledzić, czy współczynnik puli wymaga odwrócenia.

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 )
Pokaż wszystko

Aby dowiedzieć się, czy pula wymaga odwrócenia, musimy uzyskać te dane wejściowe do read_pool. Ponadto symbol aktywa musi być poprawnie skonfigurowany.

Składnia <a> if <b> else <c> jest odpowiednikiem w Pythonie trójargumentowego operatora warunkowego (opens in a new tab), który w języku pochodnym C byłby <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

Funkcja ta tworzy ciąg znaków, który formatuje listę obiektów Quote, zakładając, że wszystkie dotyczą tego samego aktywa.

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

W Pythonie wieloliniowe literały ciągów znaków (opens in a new tab) są zapisywane jako """ .... """.

1Biorąc pod uwagę te notowania:
2{
3 functools.reduce(lambda acc, q: acc + '\n' + q,
4 map(lambda q: format_quotes(q), quotes))
5}

W tym miejscu używamy wzorca MapReduce (opens in a new tab) do wygenerowania ciągu znaków dla każdej listy notowań za pomocą format_quotes, a następnie redukujemy je do pojedynczego ciągu znaków do użycia w monicie.

1Jakiej wartości dla {asset} spodziewałbyś się o godzinie {expected_time}?
2
3Podaj swoją odpowiedź jako pojedynczą liczbę zaokrągloną do dwóch miejsc po przecinku,
4bez żadnego innego tekstu.
5 """

Reszta monitu jest zgodna z oczekiwaniami.

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)
Pokaż wszystko

Przejrzyj dwie pule i uzyskaj notowania z obu.

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

Określ przyszły punkt czasowy, dla którego chcemy uzyskać oszacowanie i utwórz monit.

Interfejs z LLM

Następnie monitujemy rzeczywisty LLM i otrzymujemy oczekiwaną przyszłą wartość. Napisałem ten program przy użyciu OpenAI, więc jeśli chcesz użyć innego dostawcy, będziesz musiał go dostosować.

  1. Załóż konto OpenAI (opens in a new tab)

  2. Zasil konto (opens in a new tab) — minimalna kwota w momencie pisania tego tekstu to 5 USD

  3. Utwórz klucz API (opens in a new tab)

  4. W wierszu poleceń wyeksportuj klucz API, aby program mógł z niego korzystać

    1export OPENAI_API_KEY=sk-<reszta klucza znajduje się tutaj>
  5. Zamelduj i uruchom agenta

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

Oto nowy kod.

1from openai import OpenAI
2
3open_ai = OpenAI() # Klient odczytuje zmienną środowiskową OPENAI_API_KEY

Zaimportuj i utwórz instancję 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)

Wywołaj API OpenAI (open_ai.chat.completions.create), aby utworzyć odpowiedź.

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")
Pokaż wszystko

Wyświetl cenę i podaj rekomendację kupna lub sprzedaży.

Testowanie prognoz

Teraz, gdy możemy generować prognozy, możemy również wykorzystać dane historyczne, aby ocenić, czy tworzymy użyteczne prognozy.

1uv run test-predictor.py

Oczekiwany wynik jest podobny do:

1Prognoza na 2026-01-05T19:50: prognozowano 3138,93 USD, realnie 3218,92 USD, błąd 79,99 USD
2Prognoza na 2026-01-06T19:56: prognozowano 3243,39 USD, realnie 3221,08 USD, błąd 22,31 USD
3Prognoza na 2026-01-07T20:02: prognozowano 3223,24 USD, realnie 3146,89 USD, błąd 76,35 USD
4Prognoza na 2026-01-08T20:11: prognozowano 3150,47 USD, realnie 3092,04 USD, błąd 58,43 USD
5.
6.
7.
8Prognoza na 2026-01-31T22:33: prognozowano 2637,73 USD, realnie 2417,77 USD, błąd 219,96 USD
9Prognoza na 2026-02-01T22:41: prognozowano 2381,70 USD, realnie 2318,84 USD, błąd 62,86 USD
10Prognoza na 2026-02-02T22:49: prognozowano 2234,91 USD, realnie 2349,28 USD, błąd 114,37 USD
11Średni błąd prognozy dla 29 prognoz: 83.87103448275862068965517241 USD
12Średnia zmiana na rekomendację: 4,787931034482758620689655172 USD
13Standardowa wariancja zmian: 104,42 USD
14Dni zyskowne: 51,72%
15Dni stratne: 48,28%
Pokaż wszystko

Większość testera jest identyczna z agentem, ale oto części, które są nowe lub zmodyfikowane.

1CYCLES_FOR_TEST = 40 # W przypadku testu historycznego, ile cykli testujemy
2
3# Uzyskaj wiele notowań
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)
Pokaż wszystko

Patrzymy wstecz na CYCLES_FOR_TEST (określone tutaj jako 40) dni.

1# Tworzenie prognoz i sprawdzanie ich pod kątem rzeczywistej historii
2
3total_error = Decimal(0)
4changes = []

Interesują nas dwa rodzaje błędów. Pierwszy, total_error, to po prostu suma błędów popełnionych przez predyktora.

Aby zrozumieć drugi, changes, musimy pamiętać o celu agenta. Nie chodzi o przewidywanie współczynnika WETH/USDC (ceny ETH). Chodzi o wydawanie zaleceń sprzedaży i kupna. Jeśli cena wynosi obecnie 2000 USD, a jutro przewiduje się 2010 USD, nie ma znaczenia, czy faktyczny wynik wyniesie 2020 USD i zarobimy dodatkowe pieniądze. Ale ma znaczenie, jeśli przewidywano 2010 USD i kupiono ETH na podstawie tej rekomendacji, a cena spadnie do 1990 USD.

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

Możemy przyjrzeć się tylko przypadkom, w których dostępna jest pełna historia (wartości użyte do prognozy i wartość rzeczywista do porównania). Oznacza to, że najnowszy przypadek musi być tym, który rozpoczął się CYCLES_BACK temu.

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

Użyj fragmentów (opens in a new tab), aby uzyskać taką samą liczbę próbek, jakiej używa agent. Kod między tym a następnym segmentem to ten sam kod do uzyskiwania prognoz, który mamy w agencie.

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

Uzyskaj przewidywaną cenę, cenę rzeczywistą i cenę w momencie prognozy. Potrzebujemy ceny w momencie prognozy, aby określić, czy rekomendacja dotyczyła kupna czy sprzedaży.

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

Ustal błąd i dodaj go do sumy.

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)

W przypadku changes chcemy uzyskać wpływ pieniężny kupna lub sprzedaży jednego ETH. Najpierw musimy więc określić rekomendację, a następnie ocenić, jak zmieniła się rzeczywista cena i czy rekomendacja przyniosła zysk (zmiana dodatnia) czy stratę (zmiana ujemna).

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

Zgłoś wyniki.

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%}")

Użyj filter (opens in a new tab), aby policzyć liczbę dni zyskownych i liczbę dni kosztownych. Wynikiem jest obiekt filtru, który musimy przekonwertować na listę, aby uzyskać długość.

Wysyłanie transakcji

Teraz musimy faktycznie przesłać transakcje. Nie chcę jednak wydawać prawdziwych pieniędzy na tym etapie, zanim system nie zostanie sprawdzony. Zamiast tego utworzymy lokalny fork sieci głównej i będziemy „handlować” w tej sieci.

Oto kroki, aby utworzyć lokalny fork i włączyć handel.

  1. Zainstaluj Foundry (opens in a new tab)

  2. Uruchom anvil (opens in a new tab)

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

    anvil nasłuchuje na domyślnym adresie URL dla Foundry, http://localhost:8545 (opens in a new tab), więc nie musimy określać adresu URL dla polecenia cast (opens in a new tab), którego używamy do manipulowania blockchainem.

  3. Podczas pracy w anvil dostępnych jest dziesięć kont testowych z ETH — ustaw zmienne środowiskowe dla pierwszego z nich

    1PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    2ADDRESS=`cast wallet address $PRIVATE_KEY`
  4. Są to kontrakty, których musimy użyć. SwapRouter (opens in a new tab) to kontrakt Uniswap v3, którego używamy do faktycznego handlu. Moglibyśmy handlować bezpośrednio przez pulę, ale jest to znacznie łatwiejsze.

    Dwie ostatnie zmienne to ścieżki Uniswap v3 wymagane do wymiany między WETH i USDC.

    1WETH_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
    2USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    3POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640
    4SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C05861564
    5WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    6USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
  5. Każde z kont testowych ma 10 000 ETH. Użyj kontraktu WETH, aby opakować 1000 ETH w celu uzyskania 1000 WETH do handlu.

    1cast send $WETH_ADDRESS "deposit()" --value 1000ether --private-key $PRIVATE_KEY
  6. Użyj SwapRouter do handlu 500 WETH na 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

    Wywołanie approve tworzy limit wydatków, który pozwala SwapRouter na wydanie niektórych z naszych tokenów. Kontrakty nie mogą monitorować zdarzeń, więc jeśli przekażemy tokeny bezpośrednio do kontraktu SwapRouter, nie będzie on wiedział, że został opłacony. Zamiast tego pozwalamy kontraktowi SwapRouter wydać określoną kwotę, a następnie SwapRouter to robi. Odbywa się to za pomocą funkcji wywoływanej przez SwapRouter, dzięki czemu wie on, czy się powiodła.

  7. Sprawdź, czy masz wystarczającą ilość obu tokenów.

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

Teraz, gdy mamy WETH i USDC, możemy faktycznie uruchomić agenta.

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

Wynik będzie wyglądał podobnie do:

1(ai-trading-agent) qbzzt@Ori-Cloudnomics:~/260215-ai-agent$ uv run agent.py
2Aktualna cena: 1843.16
3O 2026-02-06T23:07 spodziewana cena: 1724.41 USD
4Sada kont przed transakcją:
5Saldo USDC: 927301.578272
6Saldo WETH: 500
7Sprzedaj, spodziewam się, że cena spadnie o 118,75 USD
8Wysłano transakcję zatwierdzenia: 74e367ddbb407c1aaf567d87aa5863049991b1d2aa092b6b85195d925e2bd41f
9Transakcja zatwierdzenia wykopana.
10Wysłano transakcję sprzedaży: fad1bcf938585c9e90364b26ac7a80eea9efd34c37e5db81e58d7655bcae28bf
11Transakcja sprzedaży wykopana.
12Salda kont po transakcji:
13Saldo USDC: 929143,797116
14Saldo WETH: 499
Pokaż wszystko

Aby faktycznie z niego skorzystać, potrzeba kilku drobnych zmian.

  • W wierszu 14 zmień MAINNET_URL na prawdziwy punkt dostępu, taki jak https://eth.drpc.org
  • W wierszu 28 zmień PRIVATE_KEY na swój własny klucz prywatny
  • Chyba że jesteś bardzo bogaty i możesz kupować lub sprzedawać 1 ETH każdego dnia dla niesprawdzonego agenta, możesz chcieć zmienić 29, aby zmniejszyć WETH_TRADE_AMOUNT

Wyjaśnienie kodu

Oto nowy kod.

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

Te same zmienne, których użyliśmy w kroku 4.

1WETH_TRADE_AMOUNT=1

Kwota do handlu.

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

Aby faktycznie handlować, potrzebujemy funkcji approve. Chcemy również pokazać salda przed i po, więc potrzebujemy również balanceOf.

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

W ABI SwapRouter potrzebujemy tylko exactInput. Istnieje powiązana funkcja, exactOutput, której moglibyśmy użyć do zakupu dokładnie jednego WETH, ale dla uproszczenia używamy exactInput w obu przypadkach.

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

Definicje Web3 dla konta (opens in a new tab) i kontraktu 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 }

Parametry transakcji. Potrzebujemy tutaj funkcji, ponieważ nonce (opens in a new tab) musi się zmieniać za każdym razem.

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

Zatwierdź limit wydatków tokena dla 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)

W ten sposób wysyłamy transakcję w Web3. Najpierw używamy obiektu Contract (opens in a new tab) do zbudowania transakcji. Następnie używamy web3.eth.account.sign_transaction (opens in a new tab) do podpisania transakcji za pomocą PRIVATE_KEY. Na koniec używamy w3.eth.send_raw_transaction (opens in a new tab) do wysłania transakcji.

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) czeka na wykopanie transakcji. W razie potrzeby zwraca potwierdzenie.

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}

Są to parametry przy sprzedaży 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 }

W przeciwieństwie do SELL_PARAMS, parametry kupna mogą się zmieniać. Kwota wejściowa to koszt 1 WETH, dostępny w 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.")
Pokaż wszystko

Funkcje buy() i sell() są prawie identyczne. Najpierw zatwierdzamy wystarczający limit wydatków dla SwapRouter, a następnie wywołujemy go z poprawną ścieżką i kwotą.

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

Zgłoś salda użytkowników w obu walutach.

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()
Pokaż wszystko

Ten agent działa obecnie tylko raz. Można jednak zmienić go tak, aby działał w sposób ciągły, uruchamiając go z crontab (opens in a new tab) lub zawijając wiersze 368-400 w pętlę i używając time.sleep (opens in a new tab), aby poczekać, aż nadejdzie czas na następny cykl.

Możliwe ulepszenia

To nie jest pełna wersja produkcyjna; to tylko przykład, aby nauczyć podstaw. Oto kilka pomysłów na ulepszenia.

Inteligentniejszy handel

Istnieją dwa ważne fakty, które agent ignoruje przy podejmowaniu decyzji, co robić.

  • Wielkość przewidywanej zmiany. Agent sprzedaje stałą kwotę WETH, jeśli oczekuje się spadku ceny, niezależnie od wielkości spadku. Prawdopodobnie lepiej byłoby ignorować drobne zmiany i sprzedawać w oparciu o to, jak bardzo spodziewamy się spadku ceny.
  • Aktualne portfolio. Jeśli 10% Twojego portfela to WETH i uważasz, że cena wzrośnie, prawdopodobnie warto kupić więcej. Ale jeśli 90% Twojego portfela to WETH, możesz być wystarczająco wyeksponowany i nie ma potrzeby kupować więcej. Odwrotnie jest, jeśli spodziewasz się spadku ceny.

Co, jeśli chcesz zachować swoją strategię handlową w tajemnicy?

Dostawcy AI mogą zobaczyć zapytania, które wysyłasz do ich LLM, co może ujawnić genialny system handlowy, który opracowałeś ze swoim agentem. System handlowy, z którego korzysta zbyt wiele osób, jest bezwartościowy, ponieważ zbyt wiele osób próbuje kupować, gdy chcesz kupić (a cena rośnie) i próbuje sprzedawać, gdy chcesz sprzedać (a cena spada).

Aby uniknąć tego problemu, można uruchomić LLM lokalnie, na przykład za pomocą LM-Studio (opens in a new tab).

Od bota AI do agenta AI

Można argumentować, że jest to bot AI, a nie agent AI. Implementuje on stosunkowo prostą strategię opartą na predefiniowanych informacjach. Możemy włączyć samodoskonalenie, na przykład, dostarczając listę pul Uniswap v3 i ich najnowszych wartości i pytając, która kombinacja ma najlepszą wartość predykcyjną.

Ochrona przed poślizgiem

Obecnie nie ma ochrony przed poślizgiem (opens in a new tab). Jeśli obecne notowanie wynosi 2000 USD, a oczekiwana cena 2100 USD, agent dokona zakupu. Jeśli jednak przed zakupem przez agenta koszt wzrośnie do 2200 USD, dalszy zakup nie ma sensu.

Aby zaimplementować ochronę przed poślizgiem, należy określić wartość amountOutMinimum w wierszach 325 i 334 pliku agent.py (opens in a new tab).

Wnioski

Mam nadzieję, że wiesz już wystarczająco dużo, aby zacząć z agentami AI. Nie jest to kompleksowy przegląd tematu; istnieją całe książki poświęcone temu zagadnieniu, ale to wystarczy, aby zacząć. Powodzenia!

Zobacz więcej mojej pracy tutaj (opens in a new tab).

Strona ostatnio zaktualizowana: 10 lutego 2026

Czy ten samouczek był pomocny?