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:
- Membaca harga token saat ini dan di masa lalu, serta informasi lain yang berpotensi relevan
- Membangun kueri dengan informasi ini, beserta informasi latar belakang untuk menjelaskan bagaimana hal itu mungkin relevan
- Mengirimkan kueri dan menerima kembali proyeksi harga
- Berdagang berdasarkan rekomendasi
- Menunggu dan mengulangi
Agen ini mendemonstrasikan 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 mengeksekusi strategi perdagangan. Agen AI memungkinkan strategi perdagangan yang lebih kompleks dan dinamis, berpotensi menggunakan informasi dan algoritma yang bahkan belum dipertimbangkan oleh pengembang.
Alat-alat
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 penawaran harga 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. Bahasanya sangat jelas, dan saya akan menjelaskan dengan tepat apa yang dilakukannya.
Pustaka Web3 (opens in a new tab) adalah API Ethereum Python yang paling umum. Ini cukup mudah digunakan.
Berdagang di blockchain
Ada banyak pertukaran 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 penawaran harga (untuk melihat nilai relatif token) dan perdagangan.
OpenAI
Untuk model bahasa besar (LLM), saya memilih untuk memulai dengan OpenAI (opens in a new tab). Untuk menjalankan aplikasi dalam tutorial ini, Anda harus membayar akses API. Pembayaran minimum sebesar $5 sudah lebih dari cukup.
Pengembangan, langkah demi langkah
Untuk menyederhanakan pengembangan, kita melanjutkan secara bertahap. Setiap langkah adalah cabang (branch) di GitHub.
Memulai
Berikut adalah 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).
-
Klon repositori GitHub.
1git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started2cd 260215-ai-agent
1
23. Instal [`uv`](https://docs.astral.sh/uv/getting-started/installation/). Perintah pada sistem Anda mungkin berbeda.3
4 ```sh5 pipx install uv-
Unduh pustaka-pustaka tersebut.
1uv sync
1
25. Aktifkan lingkungan virtual.3
4 ```sh5 source .venv/bin/activate-
Untuk memverifikasi bahwa Python dan Web3 berfungsi dengan benar, jalankan
python3dan berikan program ini. Anda dapat memasukkannya pada prompt>>>; tidak perlu membuat file.1from web3 import Web32MAINNET_URL = "https://eth.drpc.org"3w3 = Web3(Web3.HTTPProvider(MAINNET_URL))4w3.eth.block_number5quit()
1
2### Membaca dari blockchain \{#read-blockchain\}3
4Langkah selanjutnya adalah membaca dari blockchain. Untuk melakukannya, Anda perlu beralih ke cabang `02-read-quote` dan kemudian menggunakan `uv` untuk menjalankan program.5
6```sh7git checkout 02-read-quote8uv 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 sysImpor pustaka yang kita butuhkan. Pustaka tersebut dijelaskan di bawah ini saat digunakan.
1print = functools.partial(print, flush=True)Mengganti print Python dengan versi yang selalu langsung mengeluarkan (flush) output. Ini berguna dalam skrip yang berjalan lama karena kita tidak ingin menunggu pembaruan status atau output debugging.
1MAINNET_URL = "https://eth.drpc.org"URL untuk menuju ke mainnet. Anda bisa mendapatkannya dari Node 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 * 24Blok mainnet 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 pasti. Ketika pengusul blok sedang down, 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. Kita meninjau penawaran harga sekali per siklus dan mencoba memperkirakan nilai pada akhir siklus berikutnya.
1# The address of the pool we're reading # Alamat pool yang sedang kita baca2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")Nilai penawaran harga diambil dari kolam 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]6
7ERC20_ABI = [8 { "name": "symbol", ... },9 { "name": "decimals", ... }10]Ini adalah ABI (opens in a new tab) untuk dua kontrak yang perlu kita hubungi. Untuk menjaga agar kode tetap ringkas, kita hanya menyertakan fungsi yang perlu kita panggil.
1w3 = Web3(Web3.HTTPProvider(MAINNET_URL))Inisialisasi pustaka Web3 (opens in a new tab) dan hubungkan ke node 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 kapital. Kelas data ini frozen (dibekukan), yang berarti bidang-bidangnya tidak dapat dimodifikasi.
Perhatikan indentasinya. 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 fungsi di Python. Definisinya diindentasi untuk menunjukkan bahwa ia masih bagian dari PoolInfo.
Dalam fungsi yang merupakan bagian dari kelas data, parameter pertama selalu self, instans kelas data yang dipanggil di sini. Di sini ada parameter lain, yaitu nomor blok.
1 assert block <= w3.eth.block_number, "Block is in the future"Jika kita bisa membaca masa depan, kita tidak akan membutuhkan AI untuk berdagang.
1 sqrt_price_x96 = Decimal(self.contract.functions.slot0().call(block_identifier=block)[0])Sintaks untuk memanggil fungsi pada EVM dari Web3 adalah ini: <contract object>.functions.<function name>().call(<parameters>). Parameternya bisa berupa parameter fungsi EVM (jika ada; di sini tidak ada) atau parameter bernama (opens in a new tab) untuk memodifikasi perilaku blockchain. 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 kedua token tersebut.
1 raw_price = (sqrt_price_x96 / Decimal(2**96)) ** 2Untuk mengurangi perhitungan onchain, Uniswap v3 tidak menyimpan faktor pertukaran aktual melainkan akar kuadratnya. Karena EVM tidak mendukung matematika floating point atau pecahan, alih-alih nilai aktual, responsnya adalah
1 # (token1 per token0) # (token1 per token0)2 return 1/(raw_price * self.decimal_factor)Harga mentah yang kita dapatkan adalah jumlah token0 yang kita dapatkan untuk setiap token1. Di kolam kita token0 adalah USDC (stablecoin dengan nilai yang sama dengan dolar AS) dan token1 adalah WETH (opens in a new tab). Nilai yang benar-benar 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 penawaran harga: harga aset tertentu pada titik waktu tertentu. Pada titik ini, bidang asset tidak relevan karena kita menggunakan satu kolam dan oleh karena itu memiliki satu aset. Namun, kita 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()5
6 return ERC20Token(7 address=address,8 symbol=symbol,9 decimals=decimals,10 contract=token11 )Fungsi ini mengambil alamat dan mengembalikan informasi tentang kontrak token di alamat tersebut. Untuk membuat Contract Web3 (opens in a new tab) 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)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 )Fungsi ini mengembalikan semua yang kita butuhkan tentang kolam 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 untuk hanya mengatur default ke w3.eth.block_number, tetapi itu tidak berfungsi 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 menjadi dua tempat desimal.
1def get_quotes(pool: PoolInfo, start_block: int, end_block: int, step: int) -> list[Quote]:Di Python Anda mendefinisikan daftar (list) (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, perulangan for (opens in a new tab) biasanya mengulangi sebuah daftar. Daftar nomor blok untuk menemukan penawaran harga 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 tersebut.
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)8
9pprint(quotes)Ini adalah kode utama dari skrip. Baca informasi kolam, dapatkan dua belas penawaran harga, dan pprint (opens in a new tab) mereka.
Membuat prompt
Selanjutnya, kita perlu mengubah daftar penawaran harga ini menjadi prompt untuk LLM dan mendapatkan nilai masa depan yang diharapkan.
1git checkout 03-create-prompt2uv run agent.pyOutputnya sekarang akan menjadi prompt ke LLM, mirip dengan:
1Given these quotes:2Asset: WETH/USDC3 2026-01-20T16:34 3016.214 .5 .6 .7 2026-02-01T17:49 2299.108
9Asset: WBTC/WETH10 2026-01-20T16:34 29.8411 .12 .13 .14 2026-02-01T17:50 33.4615
16
17What would you expect the value for WETH/USDC to be at time 2026-02-02T17:56?18
19Provide your answer as a single number rounded to two decimal places,20without any other text.Perhatikan bahwa ada penawaran harga untuk dua aset di sini, WETH/USDC dan WBTC/WETH. Menambahkan penawaran harga dari aset lain mungkin meningkatkan akurasi prediksi.
Seperti apa bentuk prompt
Prompt ini berisi tiga bagian, yang cukup umum dalam prompt LLM.
-
Informasi. LLM memiliki banyak informasi dari pelatihannya, tetapi mereka biasanya tidak memiliki yang terbaru. Inilah alasan kita perlu mengambil penawaran harga 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 mencapainya. 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# The addresses of the pools we're reading # Alamat-alamat pool yang sedang kita baca2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")3WETHWBTC_ADDRESS = Web3.to_checksum_address("0xCBCdF9626bC03E24f779434178A73a0B4bad62eD")Kita memiliki dua kolam yang perlu kita baca.
1@dataclass(frozen=True)2class PoolInfo:3 .4 .5 .6 reverse: bool = False7
8 def get_price(self, block: int) -> Decimal:9 assert block <= w3.eth.block_number, "Block is in the future"10 sqrt_price_x96 = Decimal(self.contract.functions.slot0().call(block_identifier=block)[0])11 raw_price = (sqrt_price_x96 / Decimal(2**96)) ** 2 # (token1 per token0) # (token1 per token0)12 if self.reverse:13 return 1/(raw_price * self.decimal_factor)14 else:15 return raw_price * self.decimal_factorDi kolam WETH/USDC, kita ingin tahu berapa banyak token0 (USDC) yang kita butuhkan untuk membeli satu token1 (WETH). Di kolam WETH/WBTC, kita ingin tahu berapa banyak token1 (WETH) yang kita butuhkan untuk membeli satu token0 (WBTC, yang merupakan Bitcoin terbungkus). Kita perlu melacak apakah rasio kolam perlu dibalik.
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=reverse13 )Untuk mengetahui apakah sebuah kolam perlu dibalik, kita harus mendapatkannya sebagai input ke read_pool. Selain itu, simbol aset perlu diatur dengan benar.
Sintaks <a> if <b> else <c> adalah padanan Python dari operator kondisional ternary (opens in a new tab), yang dalam bahasa turunan C akan menjadi <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 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 multi-baris (opens in a new tab) ditulis sebagai """ .... """.
1Given these quotes:2{3 functools.reduce(lambda acc, q: acc + '\n' + q,4 map(lambda q: format_quotes(q), quotes))5}Di sini, kita menggunakan pola MapReduce (opens in a new tab) untuk menghasilkan string untuk setiap daftar penawaran harga dengan format_quotes, lalu mereduksinya menjadi satu string untuk digunakan dalam prompt.
1What would you expect the value for {asset} to be at time {expected_time}?2
3Provide your answer as a single number rounded to two decimal places,4without any other text.5 """Sisa 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)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_BLOCKS15)Tinjau kedua kolam dan dapatkan penawaran harga dari keduanya.
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))Tentukan titik waktu masa depan yang kita inginkan untuk perkiraan, dan buat prompt.
Berinteraksi dengan LLM
Selanjutnya, kita memberikan prompt ke 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 perlu menyesuaikannya.
-
Dapatkan akun OpenAI (opens in a new tab)
-
Isi dana 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-<the rest of the key goes here>
1
25. Checkout dan jalankan agen3
4 ```sh5 git checkout 04-interface-llm6 uv run agent.pyBerikut adalah kode barunya.
1from openai import OpenAI2
3open_ai = OpenAI() # The client reads the OPENAI_API_KEY environment variable # Klien membaca variabel lingkungan OPENAI_API_KEYImpor dan instansiasi 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].price3
4print ("Current price:", wethusdc_quotes[-1].price)5print(f"In {future_time}, expected price: {expected_price} USD")6
7if (expected_price > current_price):8 print(f"Buy, I expect the price to go up by {expected_price - current_price} USD")9else:10 print(f"Sell, I expect the price to go down by {current_price - expected_price} USD")Keluarkan 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 mirip dengan:
1Prediction for 2026-01-05T19:50: predicted 3138.93 USD, real 3218.92 USD, error 79.99 USD2Prediction for 2026-01-06T19:56: predicted 3243.39 USD, real 3221.08 USD, error 22.31 USD3Prediction for 2026-01-07T20:02: predicted 3223.24 USD, real 3146.89 USD, error 76.35 USD4Prediction for 2026-01-08T20:11: predicted 3150.47 USD, real 3092.04 USD, error 58.43 USD5.6.7.8Prediction for 2026-01-31T22:33: predicted 2637.73 USD, real 2417.77 USD, error 219.96 USD9Prediction for 2026-02-01T22:41: predicted 2381.70 USD, real 2318.84 USD, error 62.86 USD10Prediction for 2026-02-02T22:49: predicted 2234.91 USD, real 2349.28 USD, error 114.37 USD11Mean prediction error over 29 predictions: 83.87103448275862068965517241 USD12Mean change per recommendation: 4.787931034482758620689655172 USD13Standard variance of changes: 104.42 USD14Profitable days: 51.72%15Losing days: 48.28%Sebagian besar penguji identik dengan agen, tetapi berikut adalah bagian-bagian yang baru atau dimodifikasi.
1CYCLES_FOR_TEST = 40 # For the backtest, how many cycles we test over # Untuk backtest, berapa banyak siklus yang kita uji2
3# Get lots of quotes # Dapatkan banyak kuotasi4wethusdc_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_BLOCKS18)Kita melihat CYCLES_FOR_TEST (ditentukan sebagai 40 di sini) hari ke belakang.
1# Create predictions and check them against real history # Buat prediksi dan bandingkan dengan riwayat nyata2
3total_error = Decimal(0)4changes = []Ada dua jenis kesalahan yang kita 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). Tujuannya adalah untuk mengeluarkan rekomendasi jual dan beli. Jika harga saat ini adalah $2000 dan ia memprediksi $2010 besok, kita tidak keberatan jika hasil sebenarnya adalah $2020 dan kita mendapatkan uang ekstra. Tetapi kita akan keberatan jika ia memprediksi $2010, dan membeli ETH berdasarkan rekomendasi tersebut, lalu harganya turun menjadi $1990.
1for index in range(0,len(wethusdc_quotes)-CYCLES_BACK):Kita 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 (slices) (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 dapatkan-prediksi yang sama dengan yang kita 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 sebenarnya, dan harga pada saat prediksi. Kita membutuhkan harga pada saat prediksi untuk menentukan apakah rekomendasinya adalah membeli atau menjual.
1 error = abs(predicted_price - real_price)2 total_error += error3 print (f"Prediction for {prediction_time}: predicted {predicted_price} USD, real {real_price} USD, error {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, kita menginginkan dampak moneter dari membeli atau menjual satu ETH. Jadi pertama-tama, kita perlu menentukan rekomendasi, lalu menilai bagaimana harga sebenarnya berubah, dan apakah rekomendasi tersebut menghasilkan uang (perubahan positif) atau menghabiskan uang (perubahan negatif).
1print (f"Mean prediction error over {len(wethusdc_quotes)-CYCLES_BACK} predictions: {total_error / Decimal(len(wethusdc_quotes)-CYCLES_BACK)} USD")2
3length_changes = Decimal(len(changes))4mean_change = sum(changes, Decimal(0)) / length_changes5print (f"Mean change per recommendation: {mean_change} USD")6var = sum((x - mean_change) ** 2 for x in changes) / length_changes7print (f"Standard variance of changes: {var.sqrt().quantize(Decimal("0.01"))} USD")Laporkan hasilnya.
1print (f"Profitable days: {len(list(filter(lambda x: x > 0, changes)))/length_changes:.2%}")2print (f"Losing days: {len(list(filter(lambda x: x < 0, changes)))/length_changes:.2%}")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 titik ini, sebelum sistemnya terbukti. Sebagai gantinya, kita akan membuat fork lokal dari mainnet, dan "berdagang" di jaringan tersebut.
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 12
1
2 `anvil` mendengarkan pada URL default untuk Foundry, http://localhost:8545, jadi kita tidak perlu menentukan URL untuk [perintah `cast`](https://getfoundry.sh/cast/overview) yang kita gunakan untuk memanipulasi blockchain.3
43. Saat berjalan di `anvil`, ada sepuluh akun pengujian yang memiliki ETH—atur variabel lingkungan untuk yang pertama5
6 ```sh7 PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff808 ADDRESS=`cast wallet address $PRIVATE_KEY`-
Ini adalah kontrak yang perlu kita gunakan.
SwapRouter(opens in a new tab) adalah kontrak Uniswap v3 yang kita gunakan untuk benar-benar berdagang. Kita bisa berdagang langsung melalui kolam, 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
1
25. Setiap akun pengujian memiliki 10.000 ETH. Gunakan kontrak WETH untuk membungkus 1000 ETH guna mendapatkan 1000 WETH untuk perdagangan.3
4 ```sh5 cast send $WETH_ADDRESS "deposit()" --value 1000ether --private-key $PRIVATE_KEY-
Gunakan
SwapRouteruntuk menukar 500 WETH dengan 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_KEY
1
2 Panggilan `approve` membuat kelonggaran (allowance) yang memungkinkan `SwapRouter` untuk membelanjakan sebagian token kita. Kontrak tidak dapat memantau peristiwa, jadi jika kita mentransfer token langsung ke kontrak `SwapRouter`, ia tidak akan tahu bahwa ia telah dibayar. Sebagai gantinya, kita mengizinkan kontrak `SwapRouter` untuk membelanjakan jumlah tertentu, dan kemudian `SwapRouter` melakukannya. Ini dilakukan melalui fungsi yang dipanggil oleh `SwapRouter`, sehingga ia tahu apakah itu berhasil.3
47. Verifikasi bahwa Anda memiliki cukup kedua token tersebut.5
6 ```sh7 cast call $WETH_ADDRESS "balanceOf(address)" $ADDRESS | cast from-wei8 echo `cast call $USDC_ADDRESS "balanceOf(address)" $ADDRESS | cast to-dec`/10^6 | bcSekarang 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.py2Current price: 1843.163In 2026-02-06T23:07, expected price: 1724.41 USD4Account balances before trade:5USDC Balance: 927301.5782726WETH Balance: 5007Sell, I expect the price to go down by 118.75 USD8Approve transaction sent: 74e367ddbb407c1aaf567d87aa5863049991b1d2aa092b6b85195d925e2bd41f9Approve transaction mined.10Sell transaction sent: fad1bcf938585c9e90364b26ac7a80eea9efd34c37e5db81e58d7655bcae28bf11Sell transaction mined.12Account balances after trade:13USDC Balance: 929143.79711614WETH Balance: 499Untuk benar-benar menggunakannya, Anda memerlukan beberapa perubahan kecil.
- Di baris 14, ubah
MAINNET_URLke titik akses nyata, sepertihttps://eth.drpc.org - Di 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 baris 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 pada 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 membutuhkan fungsi approve. Kita juga ingin menunjukkan saldo sebelum dan sesudah, jadi kita juga membutuhkan balanceOf.
1SWAP_ROUTER_ABI = [2 { "name": "exactInput", ...},3]Dalam ABI SwapRouter kita hanya membutuhkan exactInput. Ada fungsi terkait, exactOutput, yang bisa kita gunakan untuk membeli tepat satu WETH, tetapi demi kesederhanaan kita hanya menggunakan exactInput di 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 account (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 membutuhkan fungsi di sini karena nonce (opens in a new tab) harus berubah setiap saat.
1def approve_token(contract: Contract, amount: int):Setujui kelonggaran 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 kita mengirim transaksi di Web3. Pertama kita menggunakan objek Contract (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, kita menggunakan w3.eth.send_raw_transaction (opens in a new tab) untuk mengirim transaksi.
1 print(f"Approve transaction sent: {tx_hash.hex()}")2 w3.eth.wait_for_transaction_receipt(tx_hash)3 print("Approve transaction mined.")w3.eth.wait_for_transaction_receipt (opens in a new tab) 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 beli dapat berubah. Jumlah input 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"Buy transaction sent: {tx_hash.hex()}")8 w3.eth.wait_for_transaction_receipt(tx_hash)9 print("Buy transaction mined.")10
11
12def sell():13 approve_token(wethusdc_pool.token1.contract,14 WETH_TRADE_AMOUNT * 10**wethusdc_pool.token1.decimals)15 txn = swap_router.functions.exactInput(SELL_PARAMS).build_transaction(txn_params())16 signed_txn = w3.eth.account.sign_transaction(txn, private_key=PRIVATE_KEY)17 tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction)18 print(f"Sell transaction sent: {tx_hash.hex()}")19 w3.eth.wait_for_transaction_receipt(tx_hash)20 print("Sell transaction mined.")Fungsi buy() dan sell() hampir identik. Pertama kita menyetujui kelonggaran 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()4
5 print(f"{wethusdc_pool.token0.symbol} Balance: {Decimal(token0_balance) / Decimal(10 ** wethusdc_pool.token0.decimals)}")6 print(f"{wethusdc_pool.token1.symbol} Balance: {Decimal(token1_balance) / Decimal(10 ** wethusdc_pool.token1.decimals)}")Laporkan saldo pengguna di kedua mata uang.
1print("Account balances before trade:")2balances()3
4if (expected_price > current_price):5 print(f"Buy, I expect the price to go up by {expected_price - current_price} USD")6 buy(wethusdc_quotes[-1])7else:8 print(f"Sell, I expect the price to go down by {current_price - expected_price} USD")9 sell()10
11print("Account balances after trade:")12balances()Agen ini saat ini hanya bekerja sekali. Namun, Anda dapat mengubahnya agar bekerja terus menerus baik dengan menjalankannya dari crontab (opens in a new tab) atau dengan membungkus baris 368-400 dalam sebuah perulangan 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 jumlah
WETHyang tetap jika harga diperkirakan akan turun, terlepas dari besarnya penurunan tersebut. Bisa dibilang, akan lebih baik untuk mengabaikan perubahan kecil dan menjual berdasarkan seberapa banyak kita memperkirakan 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 berlaku jika Anda memperkirakan 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 nilainya karena terlalu banyak orang mencoba membeli saat Anda ingin membeli (dan harganya naik) dan mencoba menjual saat Anda ingin menjual (dan harganya 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 bagus bahwa ini adalah bot AI, bukan agen AI. Ini mengimplementasikan strategi yang relatif sederhana yang bergantung pada informasi yang telah ditentukan sebelumnya. Kita dapat mengaktifkan peningkatan diri, misalnya, dengan memberikan daftar kolam Uniswap v3 dan nilai terbarunya serta menanyakan kombinasi mana yang memiliki nilai prediktif terbaik.
Perlindungan slippage
Saat ini tidak ada perlindungan slippage (opens in a new tab). Jika penawaran harga 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 mengimplementasikan perlindungan slippage, tentukan nilai amountOutMinimum di baris 325 dan 334 dari agent.py (opens in a new tab).
Kesimpulan
Semoga, sekarang Anda tahu cukup banyak untuk memulai dengan agen AI. Ini bukan gambaran komprehensif tentang subjek tersebut; ada seluruh buku yang didedikasikan untuk itu, tetapi ini cukup untuk membantu Anda memulai. Semoga berhasil!
Lihat di sini untuk karya saya yang lain (opens in a new tab).
Pembaruan terakhir halaman: 3 Maret 2026