Přeskočit na hlavní obsah

Vytvořte si vlastního AI obchodního agenta na Ethereu

AI
obchodování
agent
python
Středně pokročilý
Ori Pomerantz
13. února 2026
21 minuta čtení

V tomto tutoriálu se naučíte, jak sestavit jednoduchého AI obchodního agenta. Tento agent funguje pomocí těchto kroků:

  1. Přečtěte si aktuální a minulé ceny tokenu, stejně jako další potenciálně relevantní informace
  2. Sestavte dotaz s těmito informacemi spolu s doplňujícími informacemi k vysvětlení, jak by to mohlo být relevantní
  3. Odešlete dotaz a obdržíte zpět předpokládanou cenu
  4. Obchodujte na základě doporučení
  5. Počkejte a opakujte

Tento agent demonstruje, jak číst informace, přeložit je do dotazu, který poskytne použitelnou odpověď, a použít tuto odpověď. Všechny tyto kroky jsou nutné pro agenta AI. Tento agent je implementován v Pythonu, protože je to nejběžnější jazyk používaný v AI.

Proč to dělat?

Automatizovaní obchodní agenti umožňují vývojářům vybrat a provést obchodní strategii. Agenti AI umožňují složitější a dynamičtější obchodní strategie, potenciálně s využitím informací a algoritmů, o jejichž použití vývojář ani neuvažoval.

Nástroje

Tento tutoriál používá Python (opens in a new tab), knihovnu Web3 (opens in a new tab) a Uniswap v3 (opens in a new tab) pro nabídky a obchodování.

Proč Python?

Nejrozšířenějším jazykem pro AI je Python (opens in a new tab), takže ho použijeme i tady. Nebojte se, pokud neznáte Python. Tento jazyk je velmi srozumitelný a já vám přesně vysvětlím, co dělá.

Knihovna Web3 (opens in a new tab) je nejběžnější Python API pro Ethereum. Její použití je celkem snadné.

Obchodování na blockchainu

Existuje mnoho decentralizovaných burz (DEX), které vám umožní obchodovat s tokeny na Ethereu. Nicméně mívají podobné směnné kurzy kvůli arbitráži.

Uniswap (opens in a new tab) je široce používaná DEX, kterou můžeme použít jak pro nabídky (pro zobrazení relativních hodnot tokenů), tak pro obchody.

OpenAI

Pro velký jazykový model jsem se rozhodl začít s OpenAI (opens in a new tab). Abyste mohli spustit aplikaci v tomto tutoriálu, budete muset zaplatit za přístup k API. Minimální platba 5 $ je více než dostačující.

Vývoj, krok za krokem

Pro zjednodušení vývoje postupujeme po etapách. Každý krok je větev v GitHubu.

Začínáme

Zde jsou kroky, jak začít v systémech UNIX nebo Linux (včetně WSL (opens in a new tab))

  1. Pokud ho ještě nemáte, stáhněte a nainstalujte Python (opens in a new tab).

  2. Naklonujte repozitář na GitHubu.

    1git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started
    2cd 260215-ai-agent
  3. Nainstalujte si uv (opens in a new tab). Příkaz na vašem systému se může lišit.

    1pipx install uv
  4. Stáhněte si knihovny.

    1uv sync
  5. Aktivujte virtuální prostředí.

    1source .venv/bin/activate
  6. Chcete-li ověřit, že Python a Web3 fungují správně, spusťte python3 a zadejte do něj tento program. Můžete jej zadat na příkazový řádek >>>, není třeba vytvářet soubor.

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

Čtení z blockchainu

Dalším krokem je čtení z blockchainu. K tomu se musíte přepnout na větev 02-read-quote a poté pomocí uv spustit program.

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

Měli byste obdržet seznam objektů Quote, každý s časovým razítkem, cenou a aktivem (v současnosti vždy WETH/USDC).

Zde je vysvětlení řádek po řádku.

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
Zobrazit vše

Importujte knihovny, které potřebujeme. Jsou vysvětleny níže, když jsou použity.

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

Nahrazuje pythonovský print verzí, která vždy okamžitě vyprázdní výstup. To je užitečné v dlouho běžícím skriptu, protože nechceme čekat na aktualizace stavu nebo na výstup pro ladění.

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

URL pro přístup na hlavní síť. Můžete si ji pořídit z uzlu jako služby nebo použít jednu z těch, které jsou inzerovány na Chainlistu (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 na hlavní síti Etherea se obvykle objeví každých dvanáct sekund, takže toto jsou počty bloků, které bychom očekávali, že se objeví v daném časovém období. Upozorňujeme, že se nejedná o přesný údaj. Když je navrhovatel bloku mimo provoz, tento blok je přeskočen a čas do dalšího bloku je 24 sekund. Kdybychom chtěli získat přesný blok pro časové razítko, použili bychom binární vyhledávání (opens in a new tab). Pro naše účely je to však dostatečně blízko. Předpovídání budoucnosti není exaktní věda.

1CYCLE_BLOCKS = DAY_BLOCKS

Velikost cyklu. Jednou za cyklus přezkoumáme nabídky a pokusíme se odhadnout hodnotu na konci dalšího cyklu.

1# Adresa fondu, který čteme
2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")

Hodnoty nabídky jsou převzaty z fondu Uniswap 3 USDC/WETH na adrese 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 (opens in a new tab). Tato adresa je již ve formě kontrolního součtu, ale je lepší použít Web3.to_checksum_address (opens in a new tab), aby byl kód znovu použitelný.

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

Toto jsou ABI (opens in a new tab) pro dvě smlouvy, které potřebujeme kontaktovat. Aby byl kód stručný, zahrnuli jsme pouze funkce, které potřebujeme volat.

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

Inicializujte knihovnu Web3 (opens in a new tab) a připojte se k uzlu Ethereum.

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

Toto je jeden ze způsobů, jak v Pythonu vytvořit datovou třídu. Datový typ Contract (opens in a new tab) se používá pro připojení ke smlouvě. Všimněte si (frozen=True). V Pythonu jsou booleany (opens in a new tab) definovány jako True nebo False s velkým písmenem. Tato datová třída je frozen (zmrazená), což znamená, že pole nelze upravovat.

Všimněte si odsazení. Na rozdíl od jazyků odvozených od C (opens in a new tab), Python používá k označení bloků odsazení. Interpret Pythonu ví, že následující definice není součástí této datové třídy, protože nezačíná na stejném odsazení jako pole datové třídy.

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) se používá pro přesnou práci s desetinnými zlomky.

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

Takto se definuje funkce v Pythonu. Definice je odsazena, aby bylo vidět, že je stále součástí PoolInfo.

Ve funkci, která je součástí datové třídy, je prvním parametrem vždy self, instance datové třídy, která zde volala. Zde je další parametr, číslo bloku.

1 assert block <= w3.eth.block_number, "Blok je v budoucnosti"

Kdybychom uměli číst budoucnost, nepotřebovali bychom AI pro obchodování.

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

Syntaxe pro volání funkce na EVM z Web3 je tato: <contract object>.functions.<function name>().call(<parameters>). Parametry mohou být parametry funkce EVM (pokud nějaké jsou; zde nejsou) nebo pojmenované parametry (opens in a new tab) pro úpravu chování blockchainu. Zde používáme jeden, block_identifier, pro určení čísla bloku, ve kterém chceme pracovat.

Výsledkem je tato struktura ve formě pole (opens in a new tab). První hodnota je funkcí směnného kurzu mezi dvěma tokeny.

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

Pro snížení výpočtů na blockchainu Uniswap v3 neukládá skutečný směnný kurz, ale spíše jeho druhou odmocninu. Protože EVM nepodporuje matematiku s plovoucí desetinnou čárkou ani zlomky, místo skutečné hodnoty je odpověď price&#x22C5296

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

Hrubá cena, kterou dostaneme, je počet token0, které získáme za každý token1. V našem fondu je token0 USDC (stabilní kryptoměna se stejnou hodnotou jako americký dolar) a token1 je WETH (opens in a new tab). Hodnota, kterou skutečně chceme, je počet dolarů za WETH, ne inverzní.

Desetinný faktor je poměr mezi desetinnými faktory (opens in a new tab) pro oba tokeny.

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

Tato datová třída představuje nabídku: cenu konkrétního aktiva v daném časovém okamžiku. V tomto okamžiku je pole asset irelevantní, protože používáme jeden fond, a proto máme jedno aktivum. Později však přidáme další aktiva.

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 )
Zobrazit vše

Tato funkce přebírá adresu a vrací informace o tokenové smlouvě na této adrese. Pro vytvoření nové smlouvy Web3 Contract (opens in a new tab) poskytneme adresu a 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 )
Zobrazit vše

Tato funkce vrací vše, co potřebujeme o konkrétním fondu (opens in a new tab). Syntaxe f"<string>" je formátovaný řetězec (opens in a new tab).

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

Získání objektu Quote. Výchozí hodnota pro block_number je None (žádná hodnota).

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

Pokud nebylo zadáno číslo bloku, použije se w3.eth.block_number, což je poslední číslo bloku. Toto je syntaxe pro příkaz if (opens in a new tab).

Mohlo by se zdát, že by bylo lepší nastavit výchozí hodnotu na w3.eth.block_number, ale to nefunguje dobře, protože by to bylo číslo bloku v době definování funkce. V dlouhodobě běžícím agentovi by to byl problém.

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 )

Použijte knihovnu datetime (opens in a new tab) k formátování do formátu čitelného pro lidi a velké jazykové modely (LLM). Použijte Decimal.quantize (opens in a new tab) k zaokrouhlení hodnoty na dvě desetinná místa.

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

V Pythonu se definuje seznam (opens in a new tab), který může obsahovat pouze určitý typ, pomocí list[<type>].

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

V Pythonu for cyklus (opens in a new tab) obvykle prochází seznam. Seznam čísel bloků, ve kterých se mají hledat nabídky, pochází z range (opens in a new tab).

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

Pro každé číslo bloku získá objekt Quote a přidá jej do seznamu quotes. Poté tento seznam vrátí.

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)
Zobrazit vše

Toto je hlavní kód skriptu. Přečte informace o fondu, získá dvanáct nabídek a pprint (opens in a new tab) je vytiskne.

Vytvoření výzvy

Dále musíme tento seznam nabídek převést na výzvu pro LLM a získat očekávanou budoucí hodnotu.

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

Výstupem nyní bude výzva pro LLM, podobná této:

1Vzhledem k těmto nabídkám:
2Aktivum: WETH/USDC
3 2026-01-20T16:34 3016.21
4 .
5 .
6 .
7 2026-02-01T17:49 2299.10
8
9Aktivum: WBTC/WETH
10 2026-01-20T16:34 29.84
11 .
12 .
13 .
14 2026-02-01T17:50 33.46
15
16
17Jakou hodnotu byste očekávali pro WETH/USDC v čase 2026-02-02T17:56?
18
19Poskytněte odpověď jako jediné číslo zaokrouhlené na dvě desetinná místa,
20bez jakéhokoli dalšího textu.
Zobrazit vše

Všimněte si, že zde jsou nabídky pro dvě aktiva, WETH/USDC a WBTC/WETH. Přidání nabídek z jiného aktiva může zlepšit přesnost předpovědi.

Jak vypadá výzva

Tato výzva obsahuje tři sekce, které jsou v LLM výzvách poměrně běžné.

  1. Informace. LLM mají spoustu informací ze svého trénování, ale obvykle nemají nejnovější. To je důvod, proč zde musíme získat nejnovější nabídky. Přidávání informací do výzvy se nazývá retrieval augmented generation (RAG) (opens in a new tab).

  2. Skutečná otázka. To je to, co chceme vědět.

  3. Pokyny pro formátování výstupu. Normálně nám LLM dá odhad s vysvětlením, jak k němu dospěl. To je lepší pro lidi, ale počítačový program potřebuje pouze výsledek.

Vysvětlení kódu

Zde je nový kód.

1from datetime import datetime, timezone, timedelta

Musíme poskytnout LLM čas, pro který chceme odhad. Pro získání času „n minut/hodin/dní“ v budoucnu používáme třídu timedelta (opens in a new tab).

1# Adresy fondů, které čteme
2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")
3WETHWBTC_ADDRESS = Web3.to_checksum_address("0xCBCdF9626bC03E24f779434178A73a0B4bad62eD")

Máme dva fondy, které musíme přečíst.

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, "Blok je v budoucnosti"
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 za token0)
12 if self.reverse:
13 return 1/(raw_price * self.decimal_factor)
14 else:
15 return raw_price * self.decimal_factor
Zobrazit vše

Ve fondu WETH/USDC chceme vědět, kolik token0 (USDC) potřebujeme k nákupu jednoho token1 (WETH). Ve fondu WETH/WBTC chceme vědět, kolik token1 (WETH) potřebujeme k nákupu jednoho token0 (WBTC, což je wrapped Bitcoin). Musíme sledovat, zda je třeba poměr fondu obrátit.

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 )
Zobrazit vše

Abychom věděli, zda je třeba fond obrátit, musíme to získat jako vstup do read_pool. Také je třeba správně nastavit symbol aktiva.

Syntaxe <a> if <b> else <c> je pythonovský ekvivalent ternárního podmíněného operátoru (opens in a new tab), který by v jazyce odvozeném od C byl <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

Tato funkce sestaví řetězec, který formátuje seznam objektů Quote, za předpokladu, že se všechny vztahují ke stejnému aktivu.

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

V Pythonu se víceřádkové řetězcové literály (opens in a new tab) zapisují jako """ .... """.

1Vzhledem k těmto nabídkám:
2{
3 functools.reduce(lambda acc, q: acc + '\n' + q,
4 map(lambda q: format_quotes(q), quotes))
5}

Zde používáme vzor MapReduce (opens in a new tab) k vygenerování řetězce pro každý seznam nabídek pomocí format_quotes, a pak je zredukujeme do jediného řetězce pro použití ve výzvě.

1Jakou hodnotu byste očekávali pro {asset} v čase {expected_time}?
2
3Poskytněte odpověď jako jediné číslo zaokrouhlené na dvě desetinná místa,
4bez jakéhokoli dalšího textu.
5 """

Zbytek výzvy je podle očekávání.

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)
Zobrazit vše

Přezkoumejte oba fondy a získejte nabídky z obou.

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

Určete budoucí časový bod, pro který chcete odhad, a vytvořte výzvu.

Propojení s LLM

Dále vyzveme skutečný LLM a získáme očekávanou budoucí hodnotu. Tento program jsem napsal pomocí OpenAI, takže pokud chcete použít jiného poskytovatele, budete ho muset upravit.

  1. Získejte účet OpenAI (opens in a new tab)

  2. Vložte peníze na účet (opens in a new tab) — minimální částka v době psaní je 5 $

  3. Vytvořte API klíč (opens in a new tab)

  4. V příkazovém řádku exportujte API klíč, aby ho váš program mohl použít

    1export OPENAI_API_KEY=sk-<sem patří zbytek klíče>
  5. Checkout a spuštění agenta

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

Zde je nový kód.

1from openai import OpenAI
2
3open_ai = OpenAI() # Klient čte proměnnou prostředí OPENAI_API_KEY

Import a instancování 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)

Zavolejte API OpenAI (open_ai.chat.completions.create) pro vytvoření odpovědi.

1expected_price = Decimal(response.choices[0].message.content.strip())
2current_price = wethusdc_quotes[-1].price
3
4print ("Aktuální cena:", wethusdc_quotes[-1].price)
5print(f"V {future_time} je očekávaná cena: {expected_price} USD")
6
7if (expected_price > current_price):
8 print(f"Nákup, očekávám, že cena vzroste o {expected_price - current_price} USD")
9else:
10 print(f"Prodej, očekávám, že cena klesne o {current_price - expected_price} USD")
Zobrazit vše

Vypište cenu a poskytněte doporučení na nákup nebo prodej.

Testování předpovědí

Nyní, když můžeme generovat předpovědi, můžeme také použít historická data k posouzení, zda produkujeme užitečné předpovědi.

1uv run test-predictor.py

Očekávaný výsledek je podobný:

1Předpověď pro 2026-01-05T19:50: předpovězeno 3138,93 USD, reálná 3218,92 USD, chyba 79,99 USD
2Předpověď pro 2026-01-06T19:56: předpovězeno 3243,39 USD, reálná 3221,08 USD, chyba 22,31 USD
3Předpověď pro 2026-01-07T20:02: předpovězeno 3223,24 USD, reálná 3146,89 USD, chyba 76,35 USD
4Předpověď pro 2026-01-08T20:11: předpovězeno 3150,47 USD, reálná 3092,04 USD, chyba 58,43 USD
5.
6.
7.
8Předpověď pro 2026-01-31T22:33: předpovězeno 2637,73 USD, reálná 2417,77 USD, chyba 219,96 USD
9Předpověď pro 2026-02-01T22:41: předpovězeno 2381,70 USD, reálná 2318,84 USD, chyba 62,86 USD
10Předpověď pro 2026-02-02T22:49: předpovězeno 2234,91 USD, reálná 2349,28 USD, chyba 114,37 USD
11Průměrná chyba předpovědi u 29 předpovědí: 83,87103448275862068965517241 USD
12Průměrná změna na doporučení: 4,787931034482758620689655172 USD
13Standardní odchylka změn: 104,42 USD
14Ziskové dny: 51,72%
15Ztrátové dny: 48,28%
Zobrazit vše

Většina testeru je identická s agentem, ale zde jsou části, které jsou nové nebo upravené.

1CYCLES_FOR_TEST = 40 # Pro zpětné testování, kolik cyklů testujeme
2
3# Získání velkého množství nabídek
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)
Zobrazit vše

Díváme se na CYCLES_FOR_TEST (zde uvedeno jako 40) dní zpět.

1# Vytvoření předpovědí a jejich kontrola vůči skutečné historii
2
3total_error = Decimal(0)
4changes = []

Zajímají nás dva typy chyb. První, total_error, je jednoduše součet chyb, které prediktor udělal.

Pro pochopení druhé, changes, si musíme připomenout účel agenta. Není to předpovídání poměru WETH/USDC (cena ETH). Je to vydávání doporučení na prodej a nákup. Pokud je cena aktuálně 2000 $ a předpovídá 2010 $ zítra, nevadí nám, pokud bude skutečný výsledek 2020 $ a vyděláme více peněz. Ale vadí nám, když předpověděl 2010 $, na základě tohoto doporučení koupil ETH a cena klesla na 1990 $.

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

Můžeme se podívat pouze na případy, kdy je k dispozici kompletní historie (hodnoty použité pro predikci a reálná hodnota pro srovnání). To znamená, že nejnovější případ musí být ten, který začal před CYCLES_BACK.

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

Použijte řezy (opens in a new tab) k získání stejného počtu vzorků, jaké používá agent. Kód mezi tímto a dalším segmentem je stejný kód pro získání predikce, který máme v agentu.

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

Získejte předpokládanou cenu, skutečnou cenu a cenu v době předpovědi. Cenu v době předpovědi potřebujeme k určení, zda bylo doporučeno nakoupit nebo prodat.

1 error = abs(predicted_price - real_price)
2 total_error += error
3 print (f"Předpověď pro {prediction_time}: předpovězeno {predicted_price} USD, skutečná {real_price} USD, chyba {error} USD")

Zjistěte chybu a přičtěte ji k celkové.

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)

Pro changes chceme peněžní dopad nákupu nebo prodeje jednoho ETH. Nejprve tedy musíme určit doporučení, poté posoudit, jak se skutečná cena změnila, a zda doporučení vydělalo peníze (pozitivní změna) nebo stálo peníze (negativní změna).

1print (f"Průměrná chyba předpovědi pro {len(wethusdc_quotes)-CYCLES_BACK} předpovědí: {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"Průměrná změna na doporučení: {mean_change} USD")
6var = sum((x - mean_change) ** 2 for x in changes) / length_changes
7print (f"Standardní odchylka změn: {var.sqrt().quantize(Decimal("0.01"))} USD")

Vypište výsledky.

1print (f"Ziskové dny: {len(list(filter(lambda x: x > 0, changes)))/length_changes:.2%}")
2print (f"Ztrátové dny: {len(list(filter(lambda x: x < 0, changes)))/length_changes:.2%}")

Použijte filter (opens in a new tab) k počítání počtu ziskových a ztrátových dnů. Výsledkem je objekt filtru, který je třeba převést na seznam, abychom získali jeho délku.

Odesílání transakcí

Nyní musíme skutečně odesílat transakce. Nechci však v tomto okamžiku utrácet skutečné peníze, než se systém osvědčí. Místo toho vytvoříme lokální větev hlavní sítě a „obchodovat“ budeme v této síti.

Zde jsou kroky k vytvoření lokální větve a povolení obchodování.

  1. Nainstalujte si Foundry (opens in a new tab)

  2. Spusťte anvil (opens in a new tab)

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

    anvil naslouchá na výchozí URL pro Foundry, http://localhost:8545 (opens in a new tab), takže nemusíme specifikovat URL pro příkaz cast (opens in a new tab), který používáme k manipulaci s blockchainem.

  3. Při běhu v anvil je k dispozici deset testovacích účtů, které mají ETH — nastavte proměnné prostředí pro první z nich

    1PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    2ADDRESS=`cast wallet address $PRIVATE_KEY`
  4. Toto jsou smlouvy, které musíme použít. SwapRouter (opens in a new tab) je smlouva Uniswap v3, kterou používáme k samotnému obchodování. Mohli bychom obchodovat přímo přes fond, ale toto je mnohem jednodušší.

    Spodní dvě proměnné jsou cesty Uniswap v3 potřebné pro směnu mezi WETH a USDC.

    1WETH_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
    2USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    3POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640
    4SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C05861564
    5WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    6USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
  5. Každý z testovacích účtů má 10 000 ETH. Použijte smlouvu WETH k zabalení 1000 ETH a získejte 1000 WETH pro obchodování.

    1cast send $WETH_ADDRESS "deposit()" --value 1000ether --private-key $PRIVATE_KEY
  6. Použijte SwapRouter k obchodování 500 WETH za 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

    Volání approve vytvoří příspěvek, který umožní SwapRouter utratit některé z našich tokenů. Smlouvy nemohou sledovat události, takže pokud bychom převedli tokeny přímo na smlouvu SwapRouter, nevěděla by, že byla zaplacena. Místo toho povolíme smlouvě SwapRouter utratit určitou částku a poté to SwapRouter udělá. To se provádí pomocí funkce volané SwapRouter, takže ví, zda byla úspěšná.

  7. Ověřte si, že máte dostatek obou tokenů.

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

Nyní, když máme WETH a USDC, můžeme skutečně spustit agenta.

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

Výstup bude vypadat podobně jako:

1(ai-trading-agent) qbzzt@Ori-Cloudnomics:~/260215-ai-agent$ uv run agent.py
2Aktuální cena: 1843.16
3V 2026-02-06T23:07, očekávaná cena: 1724.41 USD
4Stavy účtů před obchodem:
5USDC Zůstatek: 927301.578272
6WETH Zůstatek: 500
7Prodej, očekávám, že cena klesne o 118.75 USD
8Schvalovací transakce odeslána: 74e367ddbb407c1aaf567d87aa5863049991b1d2aa092b6b85195d925e2bd41f
9Schvalovací transakce vytěžena.
10Prodejní transakce odeslána: fad1bcf938585c9e90364b26ac7a80eea9efd34c37e5db81e58d7655bcae28bf
11Prodejní transakce vytěžena.
12Stavy účtů po obchodě:
13USDC Zůstatek: 929143.797116
14WETH Zůstatek: 499
Zobrazit vše

Pro skutečné použití potřebujete několik drobných změn.

  • Na řádku 14 změňte MAINNET_URL na skutečný přístupový bod, například https://eth.drpc.org
  • Na řádku 28 změňte PRIVATE_KEY na váš vlastní privátní klíč
  • Pokud nejste velmi bohatí a nemůžete si dovolit kupovat nebo prodávat 1 ETH každý den pro neprověřeného agenta, možná budete chtít změnit řádek 29 a snížit WETH_TRADE_AMOUNT

Vysvětlení kódu

Zde je nový kód.

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

Stejné proměnné, které jsme použili v kroku 4.

1WETH_TRADE_AMOUNT=1

Částka k obchodování.

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

K samotnému obchodování potřebujeme funkci approve. Chceme také zobrazit zůstatky před a po, takže potřebujeme také balanceOf.

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

V ABI SwapRouter potřebujeme pouze exactInput. Existuje příbuzná funkce exactOutput, kterou bychom mohli použít k nákupu přesně jednoho WETH, ale pro jednoduchost používáme exactInput v obou případech.

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

Definice Web3 pro účet (opens in a new tab) a smlouvu 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 transakce. Potřebujeme zde funkci, protože nonce (opens in a new tab) se musí pokaždé měnit.

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

Schvalte povolenku tokenu pro 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)

Takto posíláme transakci v Web3. Nejprve použijeme objekt Contract (opens in a new tab) k vytvoření transakce. Poté použijeme web3.eth.account.sign_transaction (opens in a new tab) k podepsání transakce pomocí PRIVATE_KEY. Nakonec použijeme w3.eth.send_raw_transaction (opens in a new tab) k odeslání transakce.

1 print(f"Schvalovací transakce odeslána: {tx_hash.hex()}")
2 w3.eth.wait_for_transaction_receipt(tx_hash)
3 print("Schvalovací transakce vytěžena.")

w3.eth.wait_for_transaction_receipt (opens in a new tab) čeká, dokud transakce není vytěžena. V případě potřeby vrátí potvrzení.

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}

Toto jsou parametry pro prodej 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 }

Na rozdíl od SELL_PARAMS, parametry pro nákup se mohou měnit. Vstupní částka je cena 1 WETH, jak je uvedeno v 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"Nákupní transakce odeslána: {tx_hash.hex()}")
8 w3.eth.wait_for_transaction_receipt(tx_hash)
9 print("Nákupní transakce vytěžena.")
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"Prodejní transakce odeslána: {tx_hash.hex()}")
19 w3.eth.wait_for_transaction_receipt(tx_hash)
20 print("Prodejní transakce vytěžena.")
Zobrazit vše

Funkce buy() a sell() jsou téměř identické. Nejprve schválíme dostatečnou povolenku pro SwapRouter a poté ho zavoláme se správnou cestou a částkou.

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

Hlásit zůstatky uživatelů v obou měnách.

1print("Stav účtu před obchodem:")
2balances()
3
4if (expected_price > current_price):
5 print(f"Nákup, očekávám, že cena vzroste o {expected_price - current_price} USD")
6 buy(wethusdc_quotes[-1])
7else:
8 print(f"Prodej, očekávám, že cena klesne o {current_price - expected_price} USD")
9 sell()
10
11print("Stav účtu po obchodě:")
12balances()
Zobrazit vše

Tento agent v současné době funguje pouze jednou. Můžete ho však upravit tak, aby pracoval nepřetržitě, buď spuštěním z crontab (opens in a new tab) nebo zabalením řádků 368–400 do smyčky a použitím time.sleep (opens in a new tab) k čekání, dokud nenastane čas na další cyklus.

Možná vylepšení

Toto není plná produkční verze; je to pouze příklad pro naučení základů. Zde jsou některé nápady na vylepšení.

Chytřejší obchodování

Existují dvě důležité skutečnosti, které agent ignoruje při rozhodování, co dělat.

  • Velikost očekávané změny. Agent prodává pevnou částku WETH, pokud se očekává pokles ceny, bez ohledu na velikost poklesu. Dalo by se namítnout, že by bylo lepší ignorovat drobné změny a prodávat na základě toho, jak moc očekáváme pokles ceny.
  • Současné portfolio. Pokud je 10 % vašeho portfolia v WETH a myslíte si, že cena poroste, pravděpodobně má smysl koupit více. Pokud je ale 90 % vašeho portfolia v WETH, můžete být dostatečně exponovaní a není třeba kupovat více. Opačně to platí, pokud očekáváte pokles ceny.

Co když chcete udržet svou obchodní strategii v tajnosti?

Prodejci AI mohou vidět dotazy, které posíláte jejich LLM, což by mohlo odhalit geniální obchodní systém, který jste vyvinuli se svým agentem. Obchodní systém, který používá příliš mnoho lidí, je bezcenný, protože příliš mnoho lidí se snaží nakupovat, když chcete nakupovat (a cena stoupá) a snaží se prodávat, když chcete prodávat (a cena klesá).

Tomuto problému se můžete vyhnout spuštěním LLM lokálně, například pomocí LM-Studio (opens in a new tab).

Od AI bota k AI agentovi

Můžete dobře argumentovat, že se jedná o AI bota, nikoli AI agenta. Implementuje relativně jednoduchou strategii, která se opírá o předdefinované informace. Můžeme umožnit sebezdokonalování, například poskytnutím seznamu fondů Uniswap v3 a jejich nejnovějších hodnot a zeptat se, která kombinace má nejlepší prediktivní hodnotu.

Ochrana proti prokluzu

V současné době neexistuje žádná ochrana proti prokluzu (opens in a new tab). Pokud je aktuální nabídka 2000 $ a očekávaná cena je 2100 $, agent nakoupí. Pokud však předtím, než agent nakoupí, cena vzroste na 2200 $, už nemá smysl nakupovat.

Pro implementaci ochrany proti prokluzu zadejte hodnotu amountOutMinimum na řádcích 325 a 334 v agent.py (opens in a new tab).

Závěr

Doufejme, že nyní víte dost na to, abyste mohli začít s agenty AI. Toto není komplexní přehled tématu; jsou o tom celé knihy, ale to stačí na to, abyste mohli začít. Hodně štěstí!

Více z mé práce najdete zde (opens in a new tab).

Stránka naposledy aktualizována: 10. února 2026

Byl tento tutoriál užitečný?