Vytvořte si vlastního AI obchodního agenta na Ethereu
V tomto tutoriálu se naučíte, jak sestavit jednoduchého AI obchodního agenta. Tento agent funguje pomocí těchto kroků:
- Přečtěte si aktuální a minulé ceny tokenu, stejně jako další potenciálně relevantní informace
- Sestavte dotaz s těmito informacemi spolu s doplňujícími informacemi k vysvětlení, jak by to mohlo být relevantní
- Odešlete dotaz a obdržíte zpět předpokládanou cenu
- Obchodujte na základě doporučení
- 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))
-
Pokud ho ještě nemáte, stáhněte a nainstalujte Python (opens in a new tab).
-
Naklonujte repozitář na GitHubu.
1git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started2cd 260215-ai-agent -
Nainstalujte si
uv(opens in a new tab). Příkaz na vašem systému se může lišit.1pipx install uv -
Stáhněte si knihovny.
1uv sync -
Aktivujte virtuální prostředí.
1source .venv/bin/activate -
Chcete-li ověřit, že Python a Web3 fungují správně, spusťte
python3a zadejte do něj tento program. Můžete jej zadat na příkazový řádek>>>, není třeba vytvářet soubor.1from web3 import Web32MAINNET_URL = "https://eth.drpc.org"3w3 = Web3(Web3.HTTPProvider(MAINNET_URL))4w3.eth.block_number5quit()
Č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-quote2uv run agent.pyMě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 Web32from web3.contract import Contract3from decimal import Decimal, ROUND_HALF_UP4from dataclasses import dataclass5from datetime import datetime, timezone6from pprint import pprint7import time8import functools9import sysZobrazit všeImportujte 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 = 122MINUTE_BLOCKS = int(60 / BLOCK_TIME_SECONDS)3HOUR_BLOCKS = MINUTE_BLOCKS * 604DAY_BLOCKS = HOUR_BLOCKS * 24Blok 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_BLOCKSVelikost cyklu. Jednou za cyklus přezkoumáme nabídky a pokusíme se odhadnout hodnotu na konci dalšího cyklu.
1# Adresa fondu, který čteme2WETHUSDC_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]67ERC20_ABI = [8 { "name": "symbol", ... },9 { "name": "decimals", ... }10]Zobrazit všeToto 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: str4 symbol: str5 decimals: int6 contract: ContractToto 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: str4 token0: ERC20Token5 token1: ERC20Token6 contract: Contract7 asset: str8 decimal_factor: Decimal = 1Typ 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)) ** 2Pro 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ěď
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: str4 price: Decimal5 asset: strTato 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()56 return ERC20Token(7 address=address,8 symbol=symbol,9 decimals=decimals,10 contract=token11 )Zobrazit všeTato 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)78 return PoolInfo(9 address=address,10 asset=f"{token1.symbol}/{token0.symbol}",11 token0=token0,12 token1=token1,13 contract=pool_contract,14 decimal_factor=Decimal(10) ** Decimal(token0.decimals - token1.decimals)15 )Zobrazit všeTato 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_numberPokud 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.asset7 )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 quotesPro 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_BLOCKS7)89pprint(quotes)Zobrazit všeToto 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-prompt2uv run agent.pyVýstupem nyní bude výzva pro LLM, podobná této:
1Vzhledem k těmto nabídkám:2Aktivum: WETH/USDC3 2026-01-20T16:34 3016.214 .5 .6 .7 2026-02-01T17:49 2299.1089Aktivum: WBTC/WETH10 2026-01-20T16:34 29.8411 .12 .13 .14 2026-02-01T17:50 33.46151617Jakou hodnotu byste očekávali pro WETH/USDC v čase 2026-02-02T17:56?1819Poskytněte odpověď jako jediné číslo zaokrouhlené na dvě desetinná místa,20bez jakéhokoli dalšího textu.Zobrazit všeVš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é.
-
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).
-
Skutečná otázka. To je to, co chceme vědět.
-
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, timedeltaMusí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é čteme2WETHUSDC_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 = False78 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_factorZobrazit všeVe 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 .56 return PoolInfo(7 .8 .9 .1011 asset= f"{token1.symbol}/{token0.symbol}" if reverse else f"{token0.symbol}/{token1.symbol}",12 reverse=reverse13 )Zobrazit všeAbychom 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 resultTato 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}?23Poskytně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)89wethwbtc_pool = read_pool(WETHWBTC_ADDRESS)10wethwbtc_quotes = get_quotes(11 wethwbtc_pool,12 w3.eth.block_number - 12*CYCLE_BLOCKS,13 w3.eth.block_number,14 CYCLE_BLOCKS15)Zobrazit všePřezkoumejte oba fondy a získejte nabídky z obou.
1future_time = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat()[0:16]23print(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.
-
Získejte účet OpenAI (opens in a new tab)
-
Vložte peníze na účet (opens in a new tab) — minimální částka v době psaní je 5 $
-
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> -
Checkout a spuštění agenta
1git checkout 04-interface-llm2uv run agent.py
Zde je nový kód.
1from openai import OpenAI23open_ai = OpenAI() # Klient čte proměnnou prostředí OPENAI_API_KEYImport 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].price34print ("Aktuální cena:", wethusdc_quotes[-1].price)5print(f"V {future_time} je očekávaná cena: {expected_price} USD")67if (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šeVypiš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.pyOč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 USD2Předpověď pro 2026-01-06T19:56: předpovězeno 3243,39 USD, reálná 3221,08 USD, chyba 22,31 USD3Předpověď pro 2026-01-07T20:02: předpovězeno 3223,24 USD, reálná 3146,89 USD, chyba 76,35 USD4Předpověď pro 2026-01-08T20:11: předpovězeno 3150,47 USD, reálná 3092,04 USD, chyba 58,43 USD5.6.7.8Předpověď pro 2026-01-31T22:33: předpovězeno 2637,73 USD, reálná 2417,77 USD, chyba 219,96 USD9Předpověď pro 2026-02-01T22:41: předpovězeno 2381,70 USD, reálná 2318,84 USD, chyba 62,86 USD10Předpověď pro 2026-02-02T22:49: předpovězeno 2234,91 USD, reálná 2349,28 USD, chyba 114,37 USD11Průměrná chyba předpovědi u 29 předpovědí: 83,87103448275862068965517241 USD12Průměrná změna na doporučení: 4,787931034482758620689655172 USD13Standardní odchylka změn: 104,42 USD14Ziskové dny: 51,72%15Ztrátové dny: 48,28%Zobrazit všeVě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ů testujeme23# Získání velkého množství nabídek4wethusdc_pool = read_pool(WETHUSDC_ADDRESS, True)5wethusdc_quotes = get_quotes(6 wethusdc_pool,7 w3.eth.block_number - CYCLE_BLOCKS*CYCLES_FOR_TEST,8 w3.eth.block_number,9 CYCLE_BLOCKS,10)1112wethwbtc_pool = read_pool(WETHWBTC_ADDRESS)13wethwbtc_quotes = get_quotes(14 wethwbtc_pool,15 w3.eth.block_number - CYCLE_BLOCKS*CYCLES_FOR_TEST,16 w3.eth.block_number,17 CYCLE_BLOCKS18)Zobrazit všeDí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é historii23total_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].price3 prediction_time_price = wethusdc_quotes[index+CYCLES_BACK-1].priceZí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 += error3 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_price3 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")23length_changes = Decimal(len(changes))4mean_change = sum(changes, Decimal(0)) / length_changes5print (f"Průměrná změna na doporučení: {mean_change} USD")6var = sum((x - mean_change) ** 2 for x in changes) / length_changes7print (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í.
-
Nainstalujte si Foundry (opens in a new tab)
-
Spusťte
anvil(opens in a new tab)1anvil --fork-url https://eth.drpc.org --block-time 12anvilnaslouchá na výchozí URL pro Foundry, http://localhost:8545 (opens in a new tab), takže nemusíme specifikovat URL pro příkazcast(opens in a new tab), který používáme k manipulaci s blockchainem. -
Při běhu v
anvilje k dispozici deset testovacích účtů, které mají ETH — nastavte proměnné prostředí pro první z nich1PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff802ADDRESS=`cast wallet address $PRIVATE_KEY` -
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=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc22USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f56404SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C058615645WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB486USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 -
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 -
Použijte
SwapRouterk obchodování 500 WETH za USDC.1cast send $WETH_ADDRESS "approve(address,uint256)" $SWAP_ROUTER 500ether --private-key $PRIVATE_KEY2MAXINT=`cast max-int uint256`3cast send $SWAP_ROUTER \4 "exactInput((bytes,address,uint256,uint256,uint256))" \5 "($WETH_TO_USDC,$ADDRESS,$MAXINT,500ether,1000000)" \6 --private-key $PRIVATE_KEYVolání
approvevytvoří příspěvek, který umožníSwapRouterutratit některé z našich tokenů. Smlouvy nemohou sledovat události, takže pokud bychom převedli tokeny přímo na smlouvuSwapRouter, nevěděla by, že byla zaplacena. Místo toho povolíme smlouvěSwapRouterutratit určitou částku a poté toSwapRouterudělá. To se provádí pomocí funkce volanéSwapRouter, takže ví, zda byla úspěšná. -
Ověřte si, že máte dostatek obou tokenů.
1cast call $WETH_ADDRESS "balanceOf(address)" $ADDRESS | cast from-wei2echo `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-trade2uv run agent.pyVýstup bude vypadat podobně jako:
1(ai-trading-agent) qbzzt@Ori-Cloudnomics:~/260215-ai-agent$ uv run agent.py2Aktuální cena: 1843.163V 2026-02-06T23:07, očekávaná cena: 1724.41 USD4Stavy účtů před obchodem:5USDC Zůstatek: 927301.5782726WETH Zůstatek: 5007Prodej, očekávám, že cena klesne o 118.75 USD8Schvalovací transakce odeslána: 74e367ddbb407c1aaf567d87aa5863049991b1d2aa092b6b85195d925e2bd41f9Schvalovací transakce vytěžena.10Prodejní transakce odeslána: fad1bcf938585c9e90364b26ac7a80eea9efd34c37e5db81e58d7655bcae28bf11Prodejní transakce vytěžena.12Stavy účtů po obchodě:13USDC Zůstatek: 929143.79711614WETH Zůstatek: 499Zobrazit všePro skutečné použití potřebujete několik drobných změn.
- Na řádku 14 změňte
MAINNET_URLna skutečný přístupový bod, napříkladhttps://eth.drpc.org - Na řádku 28 změňte
PRIVATE_KEYna 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_ABI5)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.")101112def sell():13 approve_token(wethusdc_pool.token1.contract,14 WETH_TRADE_AMOUNT * 10**wethusdc_pool.token1.decimals)15 txn = swap_router.functions.exactInput(SELL_PARAMS).build_transaction(txn_params())16 signed_txn = w3.eth.account.sign_transaction(txn, private_key=PRIVATE_KEY)17 tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction)18 print(f"Prodejní transakce odeslána: {tx_hash.hex()}")19 w3.eth.wait_for_transaction_receipt(tx_hash)20 print("Prodejní transakce vytěžena.")Zobrazit všeFunkce 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()45 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()34if (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()1011print("Stav účtu po obchodě:")12balances()Zobrazit všeTento 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