Buat agen perdagangan AI Anda sendiri di Ethereum
Dalam tutorial ini Anda akan belajar cara membangun agen perdagangan AI sederhana. Agen ini bekerja menggunakan langkah-langkah berikut:
- Baca harga token saat ini dan yang lalu, serta informasi lain yang berpotensi relevan
- Buat kueri dengan informasi ini, beserta informasi latar belakang untuk menjelaskan bagaimana informasi tersebut mungkin relevan
- Kirimkan kueri dan terima kembali harga yang diproyeksikan
- Perdagangkan berdasarkan rekomendasi
- Tunggu dan ulangi
Agen ini menunjukkan cara membaca informasi, menerjemahkannya ke dalam kueri yang menghasilkan jawaban yang dapat digunakan, dan menggunakan jawaban tersebut. Semua ini adalah langkah-langkah yang diperlukan untuk agen AI. Agen ini diimplementasikan dalam Python karena ini adalah bahasa yang paling umum digunakan dalam AI.
Mengapa melakukan ini?
Agen perdagangan otomatis memungkinkan pengembang untuk memilih dan menjalankan strategi perdagangan. Agen AI memungkinkan strategi perdagangan yang lebih kompleks dan dinamis, berpotensi menggunakan informasi dan algoritma yang bahkan belum dipertimbangkan oleh pengembang untuk digunakan.
Perangkat
Tutorial ini menggunakan Python (opens in a new tab), pustaka Web3 (opens in a new tab), dan Uniswap v3 (opens in a new tab) untuk kuotasi dan perdagangan.
Mengapa Python?
Bahasa yang paling banyak digunakan untuk AI adalah Python (opens in a new tab), jadi kami menggunakannya di sini. Jangan khawatir jika Anda tidak tahu Python. Bahasa ini sangat jelas, dan saya akan menjelaskan dengan tepat apa yang dilakukannya.
Pustaka Web3 (opens in a new tab) adalah API Python Ethereum yang paling umum. Cukup mudah digunakan.
Berdagang di rantai blok
Ada banyak bursa terdesentralisasi (DEX) yang memungkinkan Anda memperdagangkan token di Ethereum. Namun, mereka cenderung memiliki nilai tukar yang serupa karena arbitrase.
Uniswap (opens in a new tab) adalah DEX yang banyak digunakan yang dapat kita gunakan untuk kuotasi (untuk melihat nilai relatif token) dan perdagangan.
OpenAI
Untuk model bahasa besar, saya memilih untuk memulai dengan OpenAI (opens in a new tab). Untuk menjalankan aplikasi dalam tutorial ini, Anda harus membayar untuk akses API. Pembayaran minimum sebesar $5 sudah lebih dari cukup.
Pengembangan, langkah demi langkah
Untuk menyederhanakan pengembangan, kita akan melanjutkannya secara bertahap. Setiap langkah adalah sebuah cabang di GitHub.
Memulai
Ada langkah-langkah untuk memulai di bawah UNIX atau Linux (termasuk WSL (opens in a new tab))
-
Jika Anda belum memilikinya, unduh dan instal Python (opens in a new tab).
-
Kloning repositori GitHub.
1git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started2cd 260215-ai-agent -
Instal
uv(opens in a new tab). Perintah di sistem Anda mungkin berbeda.1pipx install uv -
Unduh pustaka.
1uv sync -
Aktifkan lingkungan virtual.
1source .venv/bin/activate -
Untuk memverifikasi Python dan Web3 berfungsi dengan benar, jalankan
python3dan berikan program ini. Anda dapat memasukkannya di prompt>>>; tidak perlu membuat file.1from web3 import Web32MAINNET_URL = "https://eth.drpc.org"3w3 = Web3(Web3.HTTPProvider(MAINNET_URL))4w3.eth.block_number5quit()
Membaca dari rantai blok
Langkah selanjutnya adalah membaca dari rantai blok. Untuk melakukannya, Anda perlu beralih ke cabang 02-read-quote lalu menggunakan uv untuk menjalankan program.
1git checkout 02-read-quote2uv run agent.pyAnda akan menerima daftar objek Quote, masing-masing dengan stempel waktu, harga, dan aset (saat ini selalu WETH/USDC).
Berikut adalah penjelasan baris demi baris.
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 sysTampilkan semuaImpor pustaka yang kita butuhkan. Mereka dijelaskan di bawah ini saat digunakan.
1print = functools.partial(print, flush=True)Mengganti print Python dengan versi yang selalu membersihkan keluaran dengan segera. Ini berguna dalam skrip yang berjalan lama karena kita tidak ingin menunggu pembaruan status atau keluaran debugging.
1MAINNET_URL = "https://eth.drpc.org"Sebuah URL untuk menuju ke Jaringan Utama. Anda bisa mendapatkan satu dari Simpul sebagai layanan atau menggunakan salah satu yang diiklankan di Chainlist (opens in a new tab).
1BLOCK_TIME_SECONDS = 122MINUTE_BLOCKS = int(60 / BLOCK_TIME_SECONDS)3HOUR_BLOCKS = MINUTE_BLOCKS * 604DAY_BLOCKS = HOUR_BLOCKS * 24Sebuah blok Jaringan Utama Ethereum biasanya terjadi setiap dua belas detik, jadi ini adalah jumlah blok yang kita harapkan terjadi dalam suatu periode waktu. Perhatikan bahwa ini bukan angka yang pasti. Ketika pengusul blok tidak berfungsi, blok tersebut dilewati, dan waktu untuk blok berikutnya adalah 24 detik. Jika kita ingin mendapatkan blok yang tepat untuk stempel waktu, kita akan menggunakan pencarian biner (opens in a new tab). Namun, ini sudah cukup dekat untuk tujuan kita. Memprediksi masa depan bukanlah ilmu pasti.
1CYCLE_BLOCKS = DAY_BLOCKSUkuran siklus. Kami meninjau kuotasi sekali per siklus dan mencoba memperkirakan nilai di akhir siklus berikutnya.
1# Alamat pool yang kita baca2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")Nilai kuotasi diambil dari pool Uniswap 3 USDC/WETH di alamat 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 (opens in a new tab). Alamat ini sudah dalam bentuk checksum, tetapi lebih baik menggunakan Web3.to_checksum_address (opens in a new tab) untuk membuat kode dapat digunakan kembali.
1POOL_ABI = [2 { "name": "slot0", ... },3 { "name": "token0", ... },4 { "name": "token1", ... },5]67ERC20_ABI = [8 { "name": "symbol", ... },9 { "name": "decimals", ... }10]Tampilkan semuaIni adalah ABI (opens in a new tab) untuk dua kontrak yang perlu kita hubungi. Untuk menjaga agar kode tetap ringkas, kami hanya menyertakan fungsi yang perlu kami panggil.
1w3 = Web3(Web3.HTTPProvider(MAINNET_URL))Mulai pustaka Web3 (opens in a new tab) dan sambungkan ke simpul Ethereum.
1@dataclass(frozen=True)2class ERC20Token:3 address: str4 symbol: str5 decimals: int6 contract: ContractIni adalah salah satu cara untuk membuat kelas data di Python. Tipe data Contract (opens in a new tab) digunakan untuk terhubung ke kontrak. Perhatikan (frozen=True). Dalam Python, boolean (opens in a new tab) didefinisikan sebagai True atau False, dengan huruf besar. Kelas data ini beku (frozen), artinya bidang-bidangnya tidak dapat dimodifikasi.
Perhatikan indentasi. Berbeda dengan bahasa turunan C (opens in a new tab), Python menggunakan indentasi untuk menunjukkan blok. Interpreter Python tahu bahwa definisi berikut bukan bagian dari kelas data ini karena tidak dimulai pada indentasi yang sama dengan bidang kelas data.
1@dataclass(frozen=True)2class PoolInfo:3 address: str4 token0: ERC20Token5 token1: ERC20Token6 contract: Contract7 asset: str8 decimal_factor: Decimal = 1Tipe Decimal (opens in a new tab) digunakan untuk menangani pecahan desimal secara akurat.
1 def get_price(self, block: int) -> Decimal:Ini adalah cara untuk mendefinisikan sebuah fungsi di Python. Definisi tersebut diberi indentasi untuk menunjukkan bahwa ia masih merupakan bagian dari PoolInfo.
Dalam sebuah fungsi yang merupakan bagian dari kelas data, parameter pertama selalu self, yaitu instance kelas data yang memanggilnya. Di sini ada parameter lain, yaitu nomor blok.
1 assert block <= w3.eth.block_number, "Blok ada di masa depan"Jika kita bisa membaca masa depan, kita tidak akan memerlukan AI untuk berdagang.
1 sqrt_price_x96 = Decimal(self.contract.functions.slot0().call(block_identifier=block)[0])Sintaks untuk memanggil fungsi di EVM dari Web3 adalah ini: <objek kontrak>.functions.<nama fungsi>().call(<parameter>). Parameter dapat berupa parameter fungsi EVM (jika ada; di sini tidak ada) atau [parameter bernama](https://en.wikipedia.org/wiki/Named_parameter) untuk mengubah perilaku rantai blok. Di sini kita menggunakan satu, block_identifier`, untuk menentukan nomor blok yang ingin kita jalankan.
Hasilnya adalah struct ini, dalam bentuk array (opens in a new tab). Nilai pertama adalah fungsi dari nilai tukar antara dua token.
1 raw_price = (sqrt_price_x96 / Decimal(2**96)) ** 2Untuk mengurangi kalkulasi di dalam rantai, Uniswap v3 tidak menyimpan faktor pertukaran aktual tetapi akar kuadratnya. Karena EVM tidak mendukung matematika titik mengambang atau pecahan, alih-alih nilai aktual, responsnya adalah
1 # (token1 per token0)2 return 1/(raw_price * self.decimal_factor)Harga mentah yang kita dapatkan adalah jumlah token0 yang kita peroleh untuk setiap token1. Di pool kami, token0 adalah USDC (Koin Stabil dengan nilai yang sama dengan dolar AS) dan token1 adalah WETH (opens in a new tab). Nilai yang sebenarnya kita inginkan adalah jumlah dolar per WETH, bukan kebalikannya.
Faktor desimal adalah rasio antara faktor desimal (opens in a new tab) untuk kedua token tersebut.
1@dataclass(frozen=True)2class Quote:3 timestamp: str4 price: Decimal5 asset: strKelas data ini mewakili sebuah kuotasi: harga aset tertentu pada titik waktu tertentu. Pada titik ini, bidang asset tidak relevan karena kita menggunakan satu pool dan oleh karena itu hanya memiliki satu aset. Namun, kami akan menambahkan lebih banyak aset nanti.
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 )Tampilkan semuaFungsi ini mengambil sebuah alamat dan mengembalikan informasi tentang kontrak token di alamat tersebut. Untuk membuat Kontrak Web3 (opens in a new tab) yang baru, kita memberikan alamat dan ABI ke 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 )Tampilkan semuaFungsi ini mengembalikan semua yang kita butuhkan tentang pool tertentu (opens in a new tab). Sintaks f"<string>" adalah string yang diformat (opens in a new tab).
1def get_quote(pool: PoolInfo, block_number: int = None) -> Quote:Dapatkan objek Quote. Nilai default untuk block_number adalah None (tidak ada nilai).
1 if block_number is None:2 block_number = w3.eth.block_numberJika nomor blok tidak ditentukan, gunakan w3.eth.block_number, yang merupakan nomor blok terbaru. Ini adalah sintaks untuk pernyataan if (opens in a new tab).
Mungkin terlihat seolah-olah akan lebih baik jika hanya mengatur default ke w3.eth.block_number, tetapi itu tidak bekerja dengan baik karena itu akan menjadi nomor blok pada saat fungsi didefinisikan. Dalam agen yang berjalan lama, ini akan menjadi masalah.
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 )Gunakan pustaka datetime (opens in a new tab) untuk memformatnya ke format yang dapat dibaca oleh manusia dan model bahasa besar (LLM). Gunakan Decimal.quantize (opens in a new tab) untuk membulatkan nilai ke dua tempat desimal.
1def get_quotes(pool: PoolInfo, start_block: int, end_block: int, step: int) -> list[Quote]:Di Python Anda mendefinisikan sebuah daftar (opens in a new tab) yang hanya dapat berisi tipe tertentu menggunakan list[<type>].
1 quotes = []2 for block in range(start_block, end_block + 1, step):Di Python sebuah for loop (opens in a new tab) biasanya melakukan iterasi pada sebuah daftar. Daftar nomor blok untuk menemukan kutipan berasal dari range (opens in a new tab).
1 quote = get_quote(pool, block)2 quotes.append(quote)3 return quotesUntuk setiap nomor blok, dapatkan objek Quote dan tambahkan ke daftar quotes. Kemudian kembalikan daftar itu.
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)Tampilkan semuaIni adalah kode utama dari skrip. Baca informasi pool, dapatkan dua belas kutipan, dan pprint (opens in a new tab) mereka.
Membuat prompt
Selanjutnya, kita perlu mengubah daftar kutipan ini menjadi sebuah prompt untuk LLM dan mendapatkan nilai masa depan yang diharapkan.
1git checkout 03-create-prompt2uv run agent.pyOutputnya sekarang akan menjadi prompt untuk LLM, mirip dengan:
1Mengingat kutipan ini:2Aset: WETH/USDC3 2026-01-20T16:34 3016.214 .5 .6 .7 2026-02-01T17:49 2299.1089Aset: WBTC/WETH10 2026-01-20T16:34 29.8411 .12 .13 .14 2026-02-01T17:50 33.46151617Berapa nilai yang Anda harapkan untuk WETH/USDC pada waktu 2026-02-02T17:56?1819Berikan jawaban Anda sebagai satu angka yang dibulatkan ke dua tempat desimal,20tanpa teks lain.Tampilkan semuaPerhatikan bahwa ada kutipan untuk dua aset di sini, WETH/USDC dan WBTC/WETH. Menambahkan kutipan dari aset lain mungkin meningkatkan akurasi prediksi.
Seperti apa bentuk sebuah prompt
Prompt ini berisi tiga bagian, yang cukup umum dalam prompt LLM.
-
Informasi. LLM memiliki banyak informasi dari pelatihan mereka, tetapi biasanya mereka tidak memiliki yang terbaru. Inilah alasan kita perlu mengambil kutipan terbaru di sini. Menambahkan informasi ke prompt disebut retrieval augmented generation (RAG) (opens in a new tab).
-
Pertanyaan sebenarnya. Inilah yang ingin kita ketahui.
-
Instruksi pemformatan output. Biasanya, LLM akan memberi kita perkiraan dengan penjelasan tentang bagaimana ia sampai pada perkiraan tersebut. Ini lebih baik untuk manusia, tetapi program komputer hanya membutuhkan hasil akhirnya.
Penjelasan kode
Berikut adalah kode barunya.
1from datetime import datetime, timezone, timedeltaKita perlu memberikan LLM waktu yang kita inginkan untuk perkiraan. Untuk mendapatkan waktu "n menit/jam/hari" di masa depan, kita menggunakan kelas timedelta (opens in a new tab).
1# Alamat pool yang kita baca2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")3WETHWBTC_ADDRESS = Web3.to_checksum_address("0xCBCdF9626bC03E24f779434178A73a0B4bad62eD")Kita memiliki dua pool yang perlu dibaca.
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 ada di masa depan"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_factorTampilkan semuaDi pool WETH/USDC, kami ingin tahu berapa banyak token0 (USDC) yang kami butuhkan untuk membeli satu token1 (WETH). Di pool WETH/WBTC, kami ingin tahu berapa banyak token1 (WETH) yang kami butuhkan untuk membeli satu token0 (WBTC, yaitu Bitcoin terbungkus). Kita perlu melacak apakah rasio pool perlu dibalik.
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 )Tampilkan semuaUntuk mengetahui apakah sebuah pool perlu dibalik, kita harus mendapatkannya sebagai input untuk read_pool. Selain itu, simbol aset perlu diatur dengan benar.
Sintaks <a> if <b> else <c> adalah padanan Python dari operator kondisional terner (opens in a new tab), yang dalam bahasa turunan C adalah <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 resultFungsi ini membangun sebuah string yang memformat daftar objek Quote, dengan asumsi semuanya berlaku untuk aset yang sama.
1def make_prompt(quotes: list[list[Quote]], expected_time: str, asset: str) -> str:2 return f"""Di Python, literal string multibaris (opens in a new tab) ditulis sebagai """ .... """.
1Mengingat kutipan ini:2{3 functools.reduce(lambda acc, q: acc + '\n' + q,4 map(lambda q: format_quotes(q), quotes))5}Di sini, kami menggunakan pola MapReduce (opens in a new tab) untuk menghasilkan string untuk setiap daftar kutipan dengan format_quotes, lalu mereduksinya menjadi satu string untuk digunakan dalam prompt.
1Berapa nilai yang Anda harapkan untuk {asset} pada waktu {expected_time}?23Berikan jawaban Anda sebagai satu angka yang dibulatkan ke dua tempat desimal,4tanpa teks lain.5 """Sisa dari prompt adalah seperti yang diharapkan.
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)Tampilkan semuaTinjau kedua pool dan dapatkan kuotasi dari keduanya.
1future_time = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat()[0:16]23print(make_prompt(wethusdc_quotes + wethwbtc_quotes, future_time, wethusdc_pool.asset))Tentukan titik waktu masa depan yang kita inginkan estimasinya, dan buat prompt.
Berinteraksi dengan LLM
Selanjutnya, kita akan meminta LLM yang sebenarnya dan menerima nilai masa depan yang diharapkan. Saya menulis program ini menggunakan OpenAI, jadi jika Anda ingin menggunakan penyedia yang berbeda, Anda harus menyesuaikannya.
-
Dapatkan akun OpenAI (opens in a new tab)
-
Danai akun (opens in a new tab)—jumlah minimum pada saat penulisan adalah $5
-
Di baris perintah, ekspor kunci API agar program Anda dapat menggunakannya
1export OPENAI_API_KEY=sk-<sisa kunci ada di sini> -
Checkout dan jalankan agen
1git checkout 04-interface-llm2uv run agent.py
Berikut adalah kode barunya.
1from openai import OpenAI23open_ai = OpenAI() # Klien membaca variabel lingkungan OPENAI_API_KEYImpor dan buat instance 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)Panggil API OpenAI (open_ai.chat.completions.create) untuk membuat respons.
1expected_price = Decimal(response.choices[0].message.content.strip())2current_price = wethusdc_quotes[-1].price34print ("Harga saat ini:", wethusdc_quotes[-1].price)5print(f"Pada {future_time}, harga yang diharapkan: {expected_price} USD")67if (expected_price > current_price):8 print(f"Beli, saya perkirakan harga akan naik sebesar {expected_price - current_price} USD")9else:10 print(f"Jual, saya perkirakan harga akan turun sebesar {current_price - expected_price} USD")Tampilkan semuaKeluarkan harga dan berikan rekomendasi beli atau jual.
Menguji prediksi
Sekarang setelah kita dapat menghasilkan prediksi, kita juga dapat menggunakan data historis untuk menilai apakah kita menghasilkan prediksi yang berguna.
1uv run test-predictor.pyHasil yang diharapkan serupa dengan:
1Prediksi untuk 2026-01-05T19:50: diprediksi 3138.93 USD, riil 3218.92 USD, kesalahan 79.99 USD2Prediksi untuk 2026-01-06T19:56: diprediksi 3243.39 USD, riil 3221.08 USD, kesalahan 22.31 USD3Prediksi untuk 2026-01-07T20:02: diprediksi 3223.24 USD, riil 3146.89 USD, kesalahan 76.35 USD4Prediksi untuk 2026-01-08T20:11: diprediksi 3150.47 USD, riil 3092.04 USD, kesalahan 58.43 USD5.6.7.8Prediksi untuk 2026-01-31T22:33: diprediksi 2637.73 USD, riil 2417.77 USD, kesalahan 219.96 USD9Prediksi untuk 2026-02-01T22:41: diprediksi 2381.70 USD, riil 2318.84 USD, kesalahan 62.86 USD10Prediksi untuk 2026-02-02T22:49: diprediksi 2234.91 USD, riil 2349.28 USD, kesalahan 114.37 USD11Kesalahan prediksi rata-rata selama 29 prediksi: 83.87103448275862068965517241 USD12Perubahan rata-rata per rekomendasi: 4.787931034482758620689655172 USD13Varians standar perubahan: 104.42 USD14Hari menguntungkan: 51.72%15Hari merugi: 48.28%Tampilkan semuaSebagian besar penguji identik dengan agen, tetapi berikut adalah bagian-bagian yang baru atau dimodifikasi.
1CYCLES_FOR_TEST = 40 # Untuk backtest, berapa banyak siklus yang kita uji23# Dapatkan banyak kutipan4wethusdc_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)Tampilkan semuaKami melihat ke belakang sebanyak CYCLES_FOR_TEST (ditentukan sebagai 40 di sini) hari.
1# Buat prediksi dan periksa terhadap riwayat nyata23total_error = Decimal(0)4changes = []Ada dua jenis kesalahan yang kami minati. Yang pertama, total_error, hanyalah jumlah kesalahan yang dibuat oleh prediktor.
Untuk memahami yang kedua, changes, kita perlu mengingat tujuan agen. Tujuannya bukan untuk memprediksi rasio WETH/USDC (harga ETH). Ini untuk mengeluarkan rekomendasi jual dan beli. Jika harga saat ini adalah $2000 dan memprediksi $2010 besok, kami tidak keberatan jika hasil sebenarnya adalah $2020 dan kami mendapatkan uang tambahan. Namun, kita keberatan jika diprediksi $2010, dan membeli ETH berdasarkan rekomendasi tersebut, dan harganya turun menjadi $1990.
1for index in range(0,len(wethusdc_quotes)-CYCLES_BACK):Kami hanya dapat melihat kasus di mana riwayat lengkap (nilai yang digunakan untuk prediksi dan nilai dunia nyata untuk membandingkannya) tersedia. Ini berarti kasus terbaru haruslah yang dimulai CYCLES_BACK yang lalu.
1 wethusdc_slice = wethusdc_quotes[index:index+CYCLES_BACK]2 wethwbtc_slice = wethwbtc_quotes[index:index+CYCLES_BACK]Gunakan irisan (opens in a new tab) untuk mendapatkan jumlah sampel yang sama dengan jumlah yang digunakan agen. Kode antara di sini dan segmen berikutnya adalah kode get-a-prediction yang sama yang kami miliki di agen.
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].priceDapatkan harga yang diprediksi, harga riil, dan harga pada saat prediksi. Kita memerlukan harga pada saat prediksi untuk menentukan apakah rekomendasi tersebut untuk membeli atau menjual.
1 error = abs(predicted_price - real_price)2 total_error += error3 print (f"Prediksi untuk {prediction_time}: diprediksi {predicted_price} USD, riil {real_price} USD, kesalahan {error} USD")Hitung kesalahannya, dan tambahkan ke total.
1 recomended_action = 'buy' if predicted_price > prediction_time_price else 'sell'2 price_increase = real_price - prediction_time_price3 changes.append(price_increase if recomended_action == 'buy' else -price_increase)Untuk changes, kami menginginkan dampak moneter dari membeli atau menjual satu ETH. Jadi pertama, kita perlu menentukan rekomendasi, kemudian menilai bagaimana harga sebenarnya berubah, dan apakah rekomendasi tersebut menghasilkan uang (perubahan positif) atau merugikan uang (perubahan negatif).
1print (f"Kesalahan prediksi rata-rata selama {len(wethusdc_quotes)-CYCLES_BACK} prediksi: {total_error / Decimal(len(wethusdc_quotes)-CYCLES_BACK)} USD")23length_changes = Decimal(len(changes))4mean_change = sum(changes, Decimal(0)) / length_changes5print (f"Perubahan rata-rata per rekomendasi: {mean_change} USD")6var = sum((x - mean_change) ** 2 for x in changes) / length_changes7print (f"Varians standar perubahan: {var.sqrt().quantize(Decimal("0.01"))} USD")Laporkan hasilnya.
1print (f"Hari menguntungkan: {len(list(filter(lambda x: x > 0, changes)))/length_changes:.2%}")2print (f"Hari merugi: {len(list(filter(lambda x: x < 0, changes)))/length_changes:.2%}")Gunakan filter (opens in a new tab) untuk menghitung jumlah hari yang menguntungkan dan jumlah hari yang merugikan. Hasilnya adalah objek filter, yang perlu kita ubah menjadi daftar untuk mendapatkan panjangnya.
Mengirimkan transaksi
Sekarang kita perlu benar-benar mengirimkan transaksi. Namun, saya tidak ingin menghabiskan uang sungguhan pada saat ini, sebelum sistem terbukti. Sebaliknya, kita akan membuat fork lokal dari Jaringan Utama, dan "berdagang" di jaringan itu.
Berikut adalah langkah-langkah untuk membuat fork lokal dan mengaktifkan perdagangan.
-
Instal Foundry (opens in a new tab)
-
Mulai
anvil(opens in a new tab)1anvil --fork-url https://eth.drpc.org --block-time 12anvilmendengarkan di URL default untuk Foundry, http://localhost:8545 (opens in a new tab), jadi kita tidak perlu menentukan URL untuk perintahcast(opens in a new tab) yang kita gunakan untuk memanipulasi rantai blok. -
Saat berjalan di
anvil, ada sepuluh akun uji yang memiliki ETH—atur variabel lingkungan untuk yang pertama1PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff802ADDRESS=`cast wallet address $PRIVATE_KEY` -
Ini adalah kontrak yang perlu kita gunakan.
SwapRouter(opens in a new tab) adalah kontrak Uniswap v3 yang kami gunakan untuk benar-benar berdagang. Kita bisa berdagang langsung melalui pool, tetapi ini jauh lebih mudah.Dua variabel terbawah adalah jalur Uniswap v3 yang diperlukan untuk menukar antara WETH dan USDC.
1WETH_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc22USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f56404SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C058615645WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB486USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 -
Setiap akun uji memiliki 10.000 ETH. Gunakan kontrak WETH untuk membungkus 1000 ETH untuk mendapatkan 1000 WETH untuk perdagangan.
1cast send $WETH_ADDRESS "deposit()" --value 1000ether --private-key $PRIVATE_KEY -
Gunakan
SwapRouteruntuk memperdagangkan 500 WETH untuk 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_KEYPanggilan
approvemembuat tunjangan yang memungkinkanSwapRouteruntuk membelanjakan sebagian token kita. Kontrak tidak dapat memantau aksi, jadi jika kita mentransfer token langsung ke kontrakSwapRouter, kontrak tersebut tidak akan tahu bahwa ia telah dibayar. Sebaliknya, kami mengizinkan kontrakSwapRouteruntuk membelanjakan sejumlah tertentu, dan kemudianSwapRoutermelakukannya. Ini dilakukan melalui fungsi yang dipanggil olehSwapRouter, sehingga ia tahu jika berhasil. -
Verifikasi Anda memiliki cukup kedua token.
1cast call $WETH_ADDRESS "balanceOf(address)" $ADDRESS | cast from-wei2echo `cast call $USDC_ADDRESS "balanceOf(address)" $ADDRESS | cast to-dec`/10^6 | bc
Sekarang setelah kita memiliki WETH dan USDC, kita dapat benar-benar menjalankan agen.
1git checkout 05-trade2uv run agent.pyOutputnya akan terlihat mirip dengan:
1(ai-trading-agent) qbzzt@Ori-Cloudnomics:~/260215-ai-agent$ uv run agent.py2Harga saat ini: 1843.163Pada 2026-02-06T23:07, harga yang diharapkan: 1724.41 USD4Saldo akun sebelum perdagangan:5Saldo USDC: 927301.5782726Saldo WETH: 5007Jual, saya perkirakan harga akan turun sebesar 118.75 USD8Transaksi approve terkirim: 74e367ddbb407c1aaf567d87aa5863049991b1d2aa092b6b85195d925e2bd41f9Transaksi approve ditambang.10Transaksi jual terkirim: fad1bcf938585c9e90364b26ac7a80eea9efd34c37e5db81e58d7655bcae28bf11Transaksi jual ditambang.12Saldo akun setelah perdagangan:13Saldo USDC: 929143.79711614Saldo WETH: 499Tampilkan semuaUntuk benar-benar menggunakannya, Anda memerlukan beberapa perubahan kecil.
- Di baris 14, ubah
MAINNET_URLke titik akses nyata, sepertihttps://eth.drpc.org - Pada baris 28, ubah
PRIVATE_KEYke kunci pribadi Anda sendiri - Kecuali Anda sangat kaya dan dapat membeli atau menjual 1 ETH setiap hari untuk agen yang belum terbukti, Anda mungkin ingin mengubah 29 untuk mengurangi
WETH_TRADE_AMOUNT
Penjelasan kode
Berikut adalah kode barunya.
1SWAP_ROUTER_ADDRESS=Web3.to_checksum_address("0xE592427A0AEce92De3Edee1F18E0157C05861564")2WETH_TO_USDC=bytes.fromhex("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")3USDC_TO_WETH=bytes.fromhex("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")4PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"Variabel yang sama yang kita gunakan di langkah 4.
1WETH_TRADE_AMOUNT=1Jumlah yang akan diperdagangkan.
1ERC20_ABI = [2 { "name": "symbol", ... },3 { "name": "decimals", ... },4 { "name": "balanceOf", ...},5 { "name": "approve", ...}6]Untuk benar-benar berdagang, kita memerlukan fungsi approve. Kami juga ingin menampilkan saldo sebelum dan sesudah, jadi kami juga memerlukan balanceOf.
1SWAP_ROUTER_ABI = [2 { "name": "exactInput", ...},3]Di SwapRouter ABI, kita hanya perlu exactInput. Ada fungsi terkait, exactOutput, yang bisa kita gunakan untuk membeli tepat satu WETH, tetapi untuk kesederhanaan kita hanya menggunakan exactInput dalam kedua kasus.
1account = w3.eth.account.from_key(PRIVATE_KEY)2swap_router = w3.eth.contract(3 address=SWAP_ROUTER_ADDRESS,4 abi=SWAP_ROUTER_ABI5)Definisi Web3 untuk akun (opens in a new tab) dan kontrak 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 }Parameter transaksi. Kita memerlukan fungsi di sini karena nonce (opens in a new tab) harus berubah setiap saat.
1def approve_token(contract: Contract, amount: int):Setujui alokasi token untuk 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)Beginilah cara kami mengirim transaksi di Web3. Pertama kita menggunakan objek Kontrak (opens in a new tab) untuk membangun transaksi. Kemudian kita menggunakan web3.eth.account.sign_transaction (opens in a new tab) untuk menandatangani transaksi, menggunakan PRIVATE_KEY. Terakhir, kami menggunakan w3.eth.send_raw_transaction (opens in a new tab) untuk mengirim transaksi.
1 print(f"Transaksi approve terkirim: {tx_hash.hex()}")2 w3.eth.wait_for_transaction_receipt(tx_hash)3 print("Transaksi approve ditambang.")w3.eth.wait_for_transaction_receipt (opens in a new tab) menunggu hingga transaksi ditambang. Ini mengembalikan tanda terima jika diperlukan.
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}Ini adalah parameter saat menjual 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 }Berbeda dengan SELL_PARAMS, parameter pembelian bisa berubah. Jumlah masukan adalah biaya 1 WETH, seperti yang tersedia di 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"Transaksi beli terkirim: {tx_hash.hex()}")8 w3.eth.wait_for_transaction_receipt(tx_hash)9 print("Transaksi beli ditambang.")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"Transaksi jual terkirim: {tx_hash.hex()}")19 w3.eth.wait_for_transaction_receipt(tx_hash)20 print("Transaksi jual ditambang.")Tampilkan semuaFungsi buy() dan sell() hampir identik. Pertama kita menyetujui alokasi yang cukup untuk SwapRouter, dan kemudian kita memanggilnya dengan jalur dan jumlah yang benar.
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} 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)}")Laporkan saldo pengguna dalam kedua mata uang.
1print("Saldo akun sebelum perdagangan:")2balances()34if (expected_price > current_price):5 print(f"Beli, saya perkirakan harga akan naik sebesar {expected_price - current_price} USD")6 buy(wethusdc_quotes[-1])7else:8 print(f"Jual, saya perkirakan harga akan turun sebesar {current_price - expected_price} USD")9 sell()1011print("Saldo akun setelah perdagangan:")12balances()Tampilkan semuaAgen ini saat ini hanya bekerja sekali. Namun, Anda dapat mengubahnya agar berfungsi secara terus-menerus baik dengan menjalankannya dari crontab (opens in a new tab) atau dengan membungkus baris 368-400 dalam sebuah loop dan menggunakan time.sleep (opens in a new tab) untuk menunggu hingga tiba waktunya untuk siklus berikutnya.
Kemungkinan perbaikan
Ini bukan versi produksi penuh; ini hanyalah contoh untuk mengajarkan dasar-dasarnya. Berikut adalah beberapa ide untuk perbaikan.
Perdagangan yang lebih cerdas
Ada dua fakta penting yang diabaikan agen saat memutuskan apa yang harus dilakukan.
- Besarnya perubahan yang diantisipasi. Agen menjual sejumlah
WETHyang tetap jika harga diperkirakan akan menurun, terlepas dari besarnya penurunan. Boleh dibilang, akan lebih baik untuk mengabaikan perubahan kecil dan menjual berdasarkan seberapa besar kita mengharapkan harga akan turun. - Portofolio saat ini. Jika 10% dari portofolio Anda ada di WETH dan Anda pikir harganya akan naik, mungkin masuk akal untuk membeli lebih banyak. Tetapi jika 90% dari portofolio Anda ada di WETH, Anda mungkin sudah cukup terekspos, dan tidak perlu membeli lebih banyak. Kebalikannya benar jika Anda mengharapkan harga akan turun.
Bagaimana jika Anda ingin merahasiakan strategi perdagangan Anda?
Vendor AI dapat melihat kueri yang Anda kirim ke LLM mereka, yang dapat mengekspos sistem perdagangan jenius yang Anda kembangkan dengan agen Anda. Sistem perdagangan yang digunakan terlalu banyak orang tidak ada gunanya karena terlalu banyak orang mencoba membeli saat Anda ingin membeli (dan harga naik) dan mencoba menjual saat Anda ingin menjual (dan harga turun).
Anda dapat menjalankan LLM secara lokal, misalnya, menggunakan LM-Studio (opens in a new tab), untuk menghindari masalah ini.
Dari bot AI ke agen AI
Anda dapat membuat argumen yang baik bahwa ini adalah bot AI, bukan agen AI. Ini mengimplementasikan strategi yang relatif sederhana yang mengandalkan informasi yang telah ditentukan sebelumnya. Kita dapat mengaktifkan perbaikan diri, misalnya, dengan menyediakan daftar pool Uniswap v3 dan nilai terbarunya dan menanyakan kombinasi mana yang memiliki nilai prediktif terbaik.
Perlindungan slippage
Saat ini tidak ada perlindungan slippage (opens in a new tab). Jika kutipan saat ini adalah $2000, dan harga yang diharapkan adalah $2100, agen akan membeli. Namun, jika sebelum agen membeli biayanya naik menjadi $2200, tidak masuk akal lagi untuk membeli.
Untuk menerapkan perlindungan slippage, tentukan nilai amountOutMinimum di baris 325 dan 334 dari agent.py (opens in a new tab).
Kesimpulan
Semoga, sekarang Anda cukup tahu untuk memulai dengan agen AI. Ini bukan gambaran umum yang komprehensif tentang subjek ini; ada seluruh buku yang didedikasikan untuk itu, tetapi ini cukup untuk membuat Anda memulai. Semoga beruntung!
Lihat di sini untuk lebih banyak pekerjaan saya (opens in a new tab).
Halaman pembaruan terakhir: 10 Februari 2026