Tạo tác tử giao dịch AI của riêng bạn trên Ethereum
Trong hướng dẫn này, bạn sẽ học cách xây dựng một tác tử giao dịch AI đơn giản. Tác tử này hoạt động theo các bước sau:
- Đọc giá hiện tại và quá khứ của một token, cũng như các thông tin khác có thể liên quan
- Xây dựng truy vấn với thông tin này, cùng với thông tin nền tảng để giải thích mức độ liên quan của nó
- Gửi truy vấn và nhận lại một mức giá dự kiến
- Giao dịch dựa trên đề xuất
- Chờ và lặp lại
Tác tử này minh họa cách đọc thông tin, chuyển nó thành một truy vấn mang lại câu trả lời có thể sử dụng được và sử dụng câu trả lời đó. Tất cả những bước này đều cần thiết cho một tác tử AI. Tác tử này được triển khai bằng Python vì đây là ngôn ngữ phổ biến nhất được sử dụng trong AI.
Tại sao lại làm điều này?
Các tác tử giao dịch tự động cho phép các nhà phát triển chọn và thực hiện một chiến lược giao dịch. Các tác tử AI cho phép các chiến lược giao dịch phức tạp và năng động hơn, có khả năng sử dụng thông tin và thuật toán mà nhà phát triển thậm chí còn chưa cân nhắc sử dụng.
Các công cụ
Hướng dẫn này sử dụng Python (opens in a new tab), thư viện Web3 (opens in a new tab) và Uniswap v3 (opens in a new tab) để lấy báo giá và giao dịch.
Tại sao lại là Python?
Ngôn ngữ được sử dụng rộng rãi nhất cho AI là Python (opens in a new tab), vì vậy chúng tôi sử dụng nó ở đây. Đừng lo lắng nếu bạn không biết Python. Ngôn ngữ này rất rõ ràng, và tôi sẽ giải thích chính xác những gì nó làm.
Thư viện Web3 (opens in a new tab) là Giao diện Lập trình Ứng dụng (API) Python Ethereum phổ biến nhất. Nó khá dễ sử dụng.
Giao dịch trên chuỗi khối
Có nhiều sàn giao dịch phi tập trung (DEX) cho phép bạn giao dịch token trên Ethereum. Tuy nhiên, chúng có xu hướng có tỷ giá hối đoái tương tự do kinh doanh chênh lệch giá.
Uniswap (opens in a new tab) là một sàn giao dịch phi tập trung (DEX) được sử dụng rộng rãi mà chúng ta có thể sử dụng cho cả báo giá (để xem giá trị tương đối của token) và giao dịch.
OpenAI
Đối với một mô hình ngôn ngữ lớn, tôi đã chọn bắt đầu với OpenAI (opens in a new tab). Để chạy ứng dụng trong hướng dẫn này, bạn sẽ cần trả tiền để truy cập API. Khoản thanh toán tối thiểu là 5 đô la là quá đủ.
Phát triển, từng bước một
Để đơn giản hóa việc phát triển, chúng tôi tiến hành theo từng giai đoạn. Mỗi bước là một nhánh trong GitHub.
Bắt đầu
Có các bước để bắt đầu trong UNIX hoặc Linux (bao gồm WSL (opens in a new tab))
-
Nếu bạn chưa có, hãy tải xuống và cài đặt Python (opens in a new tab).
-
Nhân bản kho lưu trữ GitHub.
1git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started2cd 260215-ai-agent -
Cài đặt
uv(opens in a new tab). Lệnh trên hệ thống của bạn có thể khác.1pipx install uv -
Tải xuống các thư viện.
1uv sync -
Kích hoạt môi trường ảo.
1source .venv/bin/activate -
Để xác minh Python và Web3 đang hoạt động chính xác, hãy chạy
python3và cung cấp cho nó chương trình này. Bạn có thể nhập nó tại dấu nhắc>>>; không cần tạo tệp.1from web3 import Web32MAINNET_URL = "https://eth.drpc.org"3w3 = Web3(Web3.HTTPProvider(MAINNET_URL))4w3.eth.block_number5quit()
Đọc từ chuỗi khối
Bước tiếp theo là đọc từ chuỗi khối. Để làm điều đó, bạn cần chuyển sang nhánh 02-read-quote và sau đó sử dụng uv để chạy chương trình.
1git checkout 02-read-quote2uv run agent.pyBạn sẽ nhận được một danh sách các đối tượng Quote, mỗi đối tượng có một dấu thời gian, một mức giá và tài sản (hiện tại luôn là WETH/USDC).
Đây là giải thích từng dòng.
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 sysHiện tất cảNhập các thư viện chúng ta cần. Chúng được giải thích bên dưới khi được sử dụng.
1print = functools.partial(print, flush=True)Thay thế hàm print của Python bằng một phiên bản luôn xóa đầu ra ngay lập tức. Điều này hữu ích trong một kịch bản chạy dài vì chúng ta không muốn chờ cập nhật trạng thái hoặc đầu ra gỡ lỗi.
1MAINNET_URL = "https://eth.drpc.org"Một URL để đến mạng chính. Bạn có thể lấy một nút từ Nút dưới dạng dịch vụ hoặc sử dụng một trong những nút được quảng cáo trong Chainlist (opens in a new tab).
1BLOCK_TIME_SECONDS = 122MINUTE_BLOCKS = int(60 / BLOCK_TIME_SECONDS)3HOUR_BLOCKS = MINUTE_BLOCKS * 604DAY_BLOCKS = HOUR_BLOCKS * 24Một khối trên mạng chính Ethereum thường diễn ra sau mỗi mười hai giây, vì vậy đây là số lượng khối mà chúng tôi mong đợi sẽ xảy ra trong một khoảng thời gian. Lưu ý rằng đây không phải là một con số chính xác. Khi người đề xuất khối bị hỏng, khối đó sẽ bị bỏ qua và thời gian cho khối tiếp theo là 24 giây. Nếu chúng tôi muốn lấy khối chính xác cho một dấu thời gian, chúng tôi sẽ sử dụng tìm kiếm nhị phân (opens in a new tab). Tuy nhiên, điều này là đủ gần cho mục đích của chúng tôi. Dự đoán tương lai không phải là một môn khoa học chính xác.
1CYCLE_BLOCKS = DAY_BLOCKSKích thước của chu kỳ. Chúng tôi xem xét các báo giá một lần mỗi chu kỳ và cố gắng ước tính giá trị vào cuối chu kỳ tiếp theo.
1# The address of the pool we're reading2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")Các giá trị báo giá được lấy từ pool Uniswap 3 USDC/WETH tại địa chỉ 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 (opens in a new tab). Địa chỉ này đã ở dạng checksum, nhưng tốt hơn là sử dụng Web3.to_checksum_address (opens in a new tab) để làm cho mã có thể tái sử dụng.
1POOL_ABI = [2 { "name": "slot0", ... },3 { "name": "token0", ... },4 { "name": "token1", ... },5]67ERC20_ABI = [8 { "name": "symbol", ... },9 { "name": "decimals", ... }10]Hiện tất cảĐây là các Giao diện nhị phân ứng dụng (ABI) (opens in a new tab) cho hai hợp đồng mà chúng ta cần liên hệ. Để giữ cho mã ngắn gọn, chúng tôi chỉ bao gồm các hàm mà chúng tôi cần gọi.
1w3 = Web3(Web3.HTTPProvider(MAINNET_URL))Khởi tạo thư viện Web3 (opens in a new tab) và kết nối với một nút Ethereum.
1@dataclass(frozen=True)2class ERC20Token:3 address: str4 symbol: str5 decimals: int6 contract: ContractĐây là một cách để tạo một lớp dữ liệu trong Python. Kiểu dữ liệu Hợp đồng (opens in a new tab) được sử dụng để kết nối với hợp đồng. Lưu ý (frozen=True). Trong Python, các kiểu dữ liệu boolean (opens in a new tab) được định nghĩa là True hoặc False, viết hoa. Lớp dữ liệu này là frozen, có nghĩa là các trường không thể bị sửa đổi.
Lưu ý phần thụt lề. Trái ngược với các ngôn ngữ có nguồn gốc từ C (opens in a new tab), Python sử dụng thụt lề để biểu thị các khối. Trình thông dịch Python biết rằng định nghĩa sau đây không phải là một phần của lớp dữ liệu này vì nó không bắt đầu ở cùng một mức thụt lề như các trường của lớp dữ liệu.
1@dataclass(frozen=True)2class PoolInfo:3 address: str4 token0: ERC20Token5 token1: ERC20Token6 contract: Contract7 asset: str8 decimal_factor: Decimal = 1Kiểu Decimal (opens in a new tab) được sử dụng để xử lý chính xác các phân số thập phân.
1 def get_price(self, block: int) -> Decimal:Đây là cách định nghĩa một hàm trong Python. Định nghĩa được thụt lề để cho thấy nó vẫn là một phần của PoolInfo.
Trong một hàm là một phần của lớp dữ liệu, tham số đầu tiên luôn là self, là thể hiện của lớp dữ liệu đã gọi ở đây. Ở đây có một tham số khác, đó là số khối.
1 assert block <= w3.eth.block_number, "Block is in the future"Nếu chúng ta có thể đọc được tương lai, chúng ta sẽ không cần AI để giao dịch.
1 sqrt_price_x96 = Decimal(self.contract.functions.slot0().call(block_identifier=block)[0])Cú pháp để gọi một hàm trên Máy chủ ảo Ethereum (EVM) từ Web3 là: <đối tượng hợp đồng>.functions.<tên hàm>().call(<tham số>). Các tham số có thể là tham số của hàm EVM (nếu có; ở đây không có) hoặc tham số được đặt tên (opens in a new tab) để sửa đổi hành vi của chuỗi khối. Ở đây chúng tôi sử dụng một tham số, block_identifier, để chỉ định số khối mà chúng tôi muốn chạy trong đó.
Kết quả là cấu trúc này, ở dạng mảng (opens in a new tab). Giá trị đầu tiên là một hàm của tỷ giá hối đoái giữa hai token.
1 raw_price = (sqrt_price_x96 / Decimal(2**96)) ** 2Để giảm các phép tính trên chuỗi, Uniswap v3 không lưu trữ hệ số trao đổi thực tế mà là căn bậc hai của nó. Bởi vì Máy chủ ảo Ethereum (EVM) không hỗ trợ toán học dấu phẩy động hoặc phân số, thay vì giá trị thực, phản hồi là
1 # (token1 per token0)2 return 1/(raw_price * self.decimal_factor)Giá thô chúng ta nhận được là số lượng token0 mà chúng ta nhận được cho mỗi token1. Trong pool của chúng tôi, token0 là USDC (stablecoin có giá trị tương đương đô la Mỹ) và token1 là WETH (opens in a new tab). Giá trị mà chúng ta thực sự muốn là số đô la cho mỗi WETH, không phải là nghịch đảo.
Hệ số thập phân là tỷ lệ giữa các hệ số thập phân (opens in a new tab) của hai token.
1@dataclass(frozen=True)2class Quote:3 timestamp: str4 price: Decimal5 asset: strLớp dữ liệu này đại diện cho một báo giá: giá của một tài sản cụ thể tại một thời điểm nhất định. Tại thời điểm này, trường asset không liên quan vì chúng tôi sử dụng một pool duy nhất và do đó có một tài sản duy nhất. Tuy nhiên, chúng tôi sẽ thêm nhiều tài sản hơn sau này.
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 )Hiện tất cảHàm này nhận một địa chỉ và trả về thông tin về hợp đồng token tại địa chỉ đó. Để tạo một Hợp đồng Web3 (opens in a new tab) mới, chúng tôi cung cấp địa chỉ và Giao diện nhị phân ứng dụng (ABI) cho 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 )Hiện tất cảHàm này trả về mọi thứ chúng ta cần về một pool cụ thể (opens in a new tab). Cú pháp f"<string>" là một chuỗi được định dạng (opens in a new tab).
1def get_quote(pool: PoolInfo, block_number: int = None) -> Quote:Nhận một đối tượng Quote. Giá trị mặc định cho block_number là None (không có giá trị).
1 if block_number is None:2 block_number = w3.eth.block_numberNếu không chỉ định số khối, hãy sử dụng w3.eth.block_number, là số khối mới nhất. Đây là cú pháp cho câu lệnh if (opens in a new tab).
Có vẻ như sẽ tốt hơn nếu chỉ đặt giá trị mặc định thành w3.eth.block_number, nhưng điều đó không hoạt động tốt vì nó sẽ là số khối tại thời điểm hàm được định nghĩa. Trong một tác tử chạy dài, đây sẽ là một vấn đề.
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 )Sử dụng thư viện datetime (opens in a new tab) để định dạng nó thành một định dạng có thể đọc được cho con người và các mô hình ngôn ngữ lớn (LLM). Sử dụng Decimal.quantize (opens in a new tab) để làm tròn giá trị đến hai chữ số thập phân.
1def get_quotes(pool: PoolInfo, start_block: int, end_block: int, step: int) -> list[Quote]:Trong Python, bạn định nghĩa một danh sách (opens in a new tab) chỉ có thể chứa một loại cụ thể bằng cách sử dụng list[<type>].
1 quotes = []2 for block in range(start_block, end_block + 1, step):Trong Python, một vòng lặp for (opens in a new tab) thường lặp qua một danh sách. Danh sách các số khối để tìm báo giá đến từ range (opens in a new tab).
1 quote = get_quote(pool, block)2 quotes.append(quote)3 return quotesVới mỗi số khối, lấy một đối tượng Quote và thêm nó vào danh sách quotes. Sau đó trả về danh sách đó.
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)Hiện tất cảĐây là mã chính của kịch bản. Đọc thông tin pool, lấy mười hai báo giá và pprint (opens in a new tab) chúng.
Tạo một lời nhắc
Tiếp theo, chúng ta cần chuyển đổi danh sách báo giá này thành một lời nhắc cho LLM và nhận được một giá trị tương lai dự kiến.
1git checkout 03-create-prompt2uv run agent.pyĐầu ra bây giờ sẽ là một lời nhắc cho LLM, tương tự như:
1Given these quotes:2Asset: WETH/USDC3 2026-01-20T16:34 3016.214 .5 .6 .7 2026-02-01T17:49 2299.1089Asset: WBTC/WETH10 2026-01-20T16:34 29.8411 .12 .13 .14 2026-02-01T17:50 33.46151617What would you expect the value for WETH/USDC to be at time 2026-02-02T17:56?1819Provide your answer as a single number rounded to two decimal places,20without any other text.Hiện tất cảLưu ý rằng có báo giá cho hai tài sản ở đây, WETH/USDC và WBTC/WETH. Thêm báo giá từ một tài sản khác có thể cải thiện độ chính xác của dự đoán.
Lời nhắc trông như thế nào
Lời nhắc này chứa ba phần, khá phổ biến trong các lời nhắc LLM.
-
Thông tin. Các LLM có rất nhiều thông tin từ quá trình đào tạo của chúng, nhưng chúng thường không có thông tin mới nhất. Đây là lý do chúng tôi cần lấy các báo giá mới nhất ở đây. Việc thêm thông tin vào một lời nhắc được gọi là tạo sinh tăng cường truy xuất (RAG) (opens in a new tab).
-
Câu hỏi thực tế. Đây là những gì chúng tôi muốn biết.
-
Hướng dẫn định dạng đầu ra. Thông thường, một LLM sẽ cho chúng ta một ước tính kèm theo giải thích về cách nó đi đến kết quả đó. Điều này tốt hơn cho con người, nhưng một chương trình máy tính chỉ cần kết quả cuối cùng.
Giải thích mã
Đây là mã mới.
1from datetime import datetime, timezone, timedeltaChúng ta cần cung cấp cho LLM thời gian mà chúng ta muốn ước tính. Để có được thời gian "n phút/giờ/ngày" trong tương lai, chúng ta sử dụng lớp timedelta (opens in a new tab).
1# The addresses of the pools we're reading2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")3WETHWBTC_ADDRESS = Web3.to_checksum_address("0xCBCdF9626bC03E24f779434178A73a0B4bad62eD")Chúng ta có hai pool cần đọc.
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, "Block is in the future"10 sqrt_price_x96 = Decimal(self.contract.functions.slot0().call(block_identifier=block)[0])11 raw_price = (sqrt_price_x96 / Decimal(2**96)) ** 2 # (token1 per token0)12 if self.reverse:13 return 1/(raw_price * self.decimal_factor)14 else:15 return raw_price * self.decimal_factorHiện tất cảTrong pool WETH/USDC, chúng tôi muốn biết chúng tôi cần bao nhiêu token0 (USDC) để mua một token1 (WETH). Trong pool WETH/WBTC, chúng tôi muốn biết chúng tôi cần bao nhiêu token1 (WETH) để mua một token0 (WBTC, là Bitcoin được bọc). Chúng ta cần theo dõi xem tỷ lệ của pool có cần được đảo ngược hay không.
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 )Hiện tất cảĐể biết một pool có cần được đảo ngược hay không, chúng ta nhận nó làm đầu vào cho read_pool. Ngoài ra, biểu tượng tài sản cần được thiết lập chính xác.
Cú pháp <a> if <b> else <c> là tương đương trong Python của toán tử điều kiện ba ngôi (opens in a new tab), mà trong một ngôn ngữ có nguồn gốc từ C sẽ là <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 resultHàm này xây dựng một chuỗi định dạng một danh sách các đối tượng Quote, giả sử tất cả chúng đều áp dụng cho cùng một tài sản.
1def make_prompt(quotes: list[list[Quote]], expected_time: str, asset: str) -> str:2 return f"""Trong Python, các chuỗi ký tự nhiều dòng (opens in a new tab) được viết là """ .... """.
1Given these quotes:2{3 functools.reduce(lambda acc, q: acc + '\n' + q,4 map(lambda q: format_quotes(q), quotes))5}Ở đây, chúng tôi sử dụng mẫu MapReduce (opens in a new tab) để tạo một chuỗi cho mỗi danh sách báo giá với format_quotes, sau đó rút gọn chúng thành một chuỗi duy nhất để sử dụng trong lời nhắc.
1What would you expect the value for {asset} to be at time {expected_time}?23Provide your answer as a single number rounded to two decimal places,4without any other text.5 """Phần còn lại của lời nhắc như mong đợi.
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)Hiện tất cảXem xét hai pool và lấy báo giá từ cả hai.
1future_time = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat()[0:16]23print(make_prompt(wethusdc_quotes + wethwbtc_quotes, future_time, wethusdc_pool.asset))Xác định thời điểm trong tương lai mà chúng ta muốn ước tính và tạo lời nhắc.
Giao tiếp với một LLM
Tiếp theo, chúng tôi nhắc một LLM thực tế và nhận được một giá trị tương lai dự kiến. Tôi đã viết chương trình này bằng OpenAI, vì vậy nếu bạn muốn sử dụng một nhà cung cấp khác, bạn sẽ cần phải điều chỉnh nó.
-
Nạp tiền vào tài khoản (opens in a new tab)—số tiền tối thiểu tại thời điểm viết bài là 5 đô la
-
Trong dòng lệnh, xuất khóa API để chương trình của bạn có thể sử dụng nó
1export OPENAI_API_KEY=sk-<the rest of the key goes here> -
Thanh toán và chạy tác tử
1git checkout 04-interface-llm2uv run agent.py
Đây là mã mới.
1from openai import OpenAI23open_ai = OpenAI() # The client reads the OPENAI_API_KEY environment variableNhập và khởi tạo 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)Gọi API OpenAI (open_ai.chat.completions.create) để tạo phản hồi.
1expected_price = Decimal(response.choices[0].message.content.strip())2current_price = wethusdc_quotes[-1].price34print ("Current price:", wethusdc_quotes[-1].price)5print(f"In {future_time}, expected price: {expected_price} USD")67if (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")Hiện tất cảXuất giá và đưa ra khuyến nghị mua hoặc bán.
Kiểm tra các dự đoán
Bây giờ chúng ta có thể tạo ra các dự đoán, chúng ta cũng có thể sử dụng dữ liệu lịch sử để đánh giá xem chúng ta có tạo ra các dự đoán hữu ích hay không.
1uv run test-predictor.pyKết quả mong đợi tương tự như:
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%Hiện tất cảHầu hết trình kiểm tra giống hệt với tác tử, nhưng đây là những phần mới hoặc đã được sửa đổi.
1CYCLES_FOR_TEST = 40 # For the backtest, how many cycles we test over23# Get lots of quotes4wethusdc_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)Hiện tất cảChúng tôi xem xét lại CYCLES_FOR_TEST (được chỉ định là 40 ở đây) ngày.
1# Create predictions and check them against real history23total_error = Decimal(0)4changes = []Có hai loại lỗi mà chúng tôi quan tâm. Thứ nhất, total_error, chỉ đơn giản là tổng các lỗi mà trình dự đoán đã mắc phải.
Để hiểu được thứ hai, changes, chúng ta cần nhớ mục đích của tác tử. Đó không phải là để dự đoán tỷ lệ WETH/USDC (giá ETH). Đó là để đưa ra các khuyến nghị bán và mua. Nếu giá hiện tại là 2000 đô la và nó dự đoán là 2010 đô la vào ngày mai, chúng tôi không phiền nếu kết quả thực tế là 2020 đô la và chúng tôi kiếm được thêm tiền. Nhưng chúng tôi có phiền nếu nó dự đoán là 2010 đô la và mua ETH dựa trên khuyến nghị đó, và giá giảm xuống còn 1990 đô la.
1for index in range(0,len(wethusdc_quotes)-CYCLES_BACK):Chúng ta chỉ có thể xem xét các trường hợp có sẵn lịch sử hoàn chỉnh (các giá trị được sử dụng để dự đoán và giá trị thực tế để so sánh với nó). Điều này có nghĩa là trường hợp mới nhất phải là trường hợp bắt đầu cách đây CYCLES_BACK.
1 wethusdc_slice = wethusdc_quotes[index:index+CYCLES_BACK]2 wethwbtc_slice = wethwbtc_quotes[index:index+CYCLES_BACK]Sử dụng các lát cắt (opens in a new tab) để có được cùng số lượng mẫu với số lượng mà tác tử sử dụng. Mã giữa đây và phân đoạn tiếp theo là cùng một mã nhận dự đoán mà chúng tôi có trong tác tử.
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].priceLấy giá dự đoán, giá thực tế và giá tại thời điểm dự đoán. Chúng ta cần giá tại thời điểm dự đoán để xác định xem khuyến nghị là mua hay bán.
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")Tính toán lỗi và cộng nó vào tổng.
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)Đối với changes, chúng tôi muốn tác động tiền tệ của việc mua hoặc bán một ETH. Vì vậy, trước tiên, chúng ta cần xác định khuyến nghị, sau đó đánh giá giá thực tế đã thay đổi như thế nào và liệu khuyến nghị đó có kiếm được tiền (thay đổi tích cực) hay tốn tiền (thay đổi tiêu cực) hay không.
1print (f"Mean prediction error over {len(wethusdc_quotes)-CYCLES_BACK} predictions: {total_error / Decimal(len(wethusdc_quotes)-CYCLES_BACK)} USD")23length_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")Báo cáo kết quả.
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%}")Sử dụng filter (opens in a new tab) để đếm số ngày có lãi và số ngày tốn kém. Kết quả là một đối tượng bộ lọc, chúng ta cần chuyển đổi nó thành một danh sách để lấy độ dài.
Gửi giao dịch
Bây giờ chúng ta cần thực sự gửi giao dịch. Tuy nhiên, tôi không muốn tiêu tiền thật vào thời điểm này, trước khi hệ thống được chứng minh. Thay vào đó, chúng tôi sẽ tạo một bản phân nhánh cục bộ của mạng chính và "giao dịch" trên mạng đó.
Đây là các bước để tạo một bản phân nhánh cục bộ và cho phép giao dịch.
-
Cài đặt Foundry (opens in a new tab)
-
Bắt đầu
anvil(opens in a new tab)1anvil --fork-url https://eth.drpc.org --block-time 12anvilđang lắng nghe trên URL mặc định cho Foundry, http://localhost:8545 (opens in a new tab), vì vậy chúng tôi không cần chỉ định URL cho lệnhcast(opens in a new tab) mà chúng tôi sử dụng để thao tác chuỗi khối. -
Khi chạy trong
anvil, có mười tài khoản thử nghiệm có ETH—thiết lập các biến môi trường cho tài khoản đầu tiên1PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff802ADDRESS=`cast wallet address $PRIVATE_KEY` -
Đây là các hợp đồng chúng ta cần sử dụng.
SwapRouter(opens in a new tab) là hợp đồng Uniswap v3 mà chúng tôi sử dụng để thực sự giao dịch. Chúng tôi có thể giao dịch trực tiếp thông qua pool, nhưng cách này dễ dàng hơn nhiều.Hai biến dưới cùng là các đường dẫn Uniswap v3 cần thiết để hoán đổi giữa WETH và USDC.
1WETH_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc22USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f56404SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C058615645WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB486USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 -
Mỗi tài khoản thử nghiệm có 10.000 ETH. Sử dụng hợp đồng WETH để bọc 1000 ETH để có được 1000 WETH để giao dịch.
1cast send $WETH_ADDRESS "deposit()" --value 1000ether --private-key $PRIVATE_KEY -
Sử dụng
SwapRouterđể giao dịch 500 WETH lấy 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_KEYLệnh gọi
approvetạo ra một khoản trợ cấp cho phépSwapRouterchi tiêu một số token của chúng tôi. Các hợp đồng không thể giám sát các sự kiện, vì vậy nếu chúng tôi chuyển token trực tiếp đến hợp đồngSwapRouter, nó sẽ không biết rằng nó đã được thanh toán. Thay vào đó, chúng tôi cho phép hợp đồngSwapRouterchi tiêu một số tiền nhất định, và sau đóSwapRoutersẽ thực hiện điều đó. Điều này được thực hiện thông qua một hàm được gọi bởiSwapRouter, vì vậy nó biết liệu nó có thành công hay không. -
Xác minh bạn có đủ cả hai loại token.
1cast call $WETH_ADDRESS "balanceOf(address)" $ADDRESS | cast from-wei2echo `cast call $USDC_ADDRESS "balanceOf(address)" $ADDRESS | cast to-dec`/10^6 | bc
Bây giờ chúng ta đã có WETH và USDC, chúng ta có thể thực sự chạy tác tử.
1git checkout 05-trade2uv run agent.pyĐầu ra sẽ trông tương tự như:
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: 499Hiện tất cảĐể thực sự sử dụng nó, bạn cần một vài thay đổi nhỏ.
- Trong dòng 14, thay đổi
MAINNET_URLthành một điểm truy cập thực, chẳng hạn nhưhttps://eth.drpc.org - Trong dòng 28, thay đổi
PRIVATE_KEYthành khóa riêng tư của riêng bạn - Trừ khi bạn rất giàu có và có thể mua hoặc bán 1 ETH mỗi ngày cho một tác tử chưa được chứng minh, bạn có thể muốn thay đổi 29 để giảm
WETH_TRADE_AMOUNT
Giải thích mã
Đây là mã mới.
1SWAP_ROUTER_ADDRESS=Web3.to_checksum_address("0xE592427A0AEce92De3Edee1F18E0157C05861564")2WETH_TO_USDC=bytes.fromhex("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")3USDC_TO_WETH=bytes.fromhex("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")4PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"Các biến tương tự chúng ta đã sử dụng trong bước 4.
1WETH_TRADE_AMOUNT=1Số tiền để giao dịch.
1ERC20_ABI = [2 { "name": "symbol", ... },3 { "name": "decimals", ... },4 { "name": "balanceOf", ...},5 { "name": "approve", ...}6]Để thực sự giao dịch, chúng ta cần hàm approve. Chúng tôi cũng muốn hiển thị số dư trước và sau, vì vậy chúng tôi cũng cần balanceOf.
1SWAP_ROUTER_ABI = [2 { "name": "exactInput", ...},3]Trong SwapRouter ABI, chúng ta chỉ cần exactInput. Có một hàm liên quan, exactOutput, chúng ta có thể sử dụng để mua chính xác một WETH, nhưng để đơn giản, chúng ta chỉ sử dụng exactInput trong cả hai trường hợp.
1account = w3.eth.account.from_key(PRIVATE_KEY)2swap_router = w3.eth.contract(3 address=SWAP_ROUTER_ADDRESS,4 abi=SWAP_ROUTER_ABI5)Các định nghĩa Web3 cho tài khoản (opens in a new tab) và hợp đồng 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 }Các tham số giao dịch. Chúng ta cần một hàm ở đây vì nonce (opens in a new tab) phải thay đổi mỗi lần.
1def approve_token(contract: Contract, amount: int):Phê duyệt một khoản trợ cấp token cho 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)Đây là cách chúng tôi gửi một giao dịch trong Web3. Đầu tiên, chúng tôi sử dụng đối tượng Hợp đồng (opens in a new tab) để xây dựng giao dịch. Sau đó, chúng tôi sử dụng web3.eth.account.sign_transaction (opens in a new tab) để ký giao dịch, sử dụng PRIVATE_KEY. Cuối cùng, chúng tôi sử dụng w3.eth.send_raw_transaction (opens in a new tab) để gửi giao dịch.
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) chờ cho đến khi giao dịch được khai thác. Nó trả về biên lai nếu cần.
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}Đây là các tham số khi bán 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 }Trái ngược với SELL_PARAMS, các tham số mua có thể thay đổi. Số tiền đầu vào là chi phí của 1 WETH, có sẵn trong 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.")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"Sell transaction sent: {tx_hash.hex()}")19 w3.eth.wait_for_transaction_receipt(tx_hash)20 print("Sell transaction mined.")Hiện tất cảCác hàm buy() và sell() gần như giống hệt nhau. Đầu tiên, chúng tôi phê duyệt một khoản trợ cấp đủ cho SwapRouter, và sau đó chúng tôi gọi nó với đường dẫn và số tiền chính xác.
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)}")Báo cáo số dư người dùng bằng cả hai loại tiền tệ.
1print("Account balances before trade:")2balances()34if (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()1011print("Account balances after trade:")12balances()Hiện tất cảTác tử này hiện chỉ hoạt động một lần. Tuy nhiên, bạn có thể thay đổi nó để hoạt động liên tục bằng cách chạy nó từ crontab (opens in a new tab) hoặc bằng cách bọc các dòng 368-400 trong một vòng lặp và sử dụng time.sleep (opens in a new tab) để đợi cho đến khi đến lúc cho chu kỳ tiếp theo.
Các cải tiến có thể có
Đây không phải là một phiên bản sản xuất đầy đủ; nó chỉ là một ví dụ để dạy những điều cơ bản. Đây là một số ý tưởng để cải tiến.
Giao dịch thông minh hơn
Có hai sự thật quan trọng mà tác tử bỏ qua khi quyết định phải làm gì.
- Mức độ thay đổi dự kiến. Tác tử bán một lượng
WETHcố định nếu giá dự kiến sẽ giảm, bất kể mức độ giảm. Có thể cho rằng, sẽ tốt hơn nếu bỏ qua những thay đổi nhỏ và bán dựa trên mức độ chúng ta mong đợi giá sẽ giảm. - Danh mục đầu tư hiện tại. Nếu 10% danh mục đầu tư của bạn là WETH và bạn nghĩ rằng giá sẽ tăng, có lẽ nên mua thêm. Nhưng nếu 90% danh mục đầu tư của bạn là WETH, bạn có thể đã đủ rủi ro và không cần phải mua thêm. Điều ngược lại cũng đúng nếu bạn mong đợi giá sẽ giảm.
Điều gì sẽ xảy ra nếu bạn muốn giữ bí mật chiến lược giao dịch của mình?
Các nhà cung cấp AI có thể thấy các truy vấn bạn gửi đến LLM của họ, điều này có thể làm lộ hệ thống giao dịch thiên tài mà bạn đã phát triển với tác tử của mình. Một hệ thống giao dịch mà quá nhiều người sử dụng là vô giá trị vì quá nhiều người cố gắng mua khi bạn muốn mua (và giá tăng lên) và cố gắng bán khi bạn muốn bán (và giá giảm xuống).
Bạn có thể chạy một LLM cục bộ, ví dụ, bằng cách sử dụng LM-Studio (opens in a new tab), để tránh vấn đề này.
Từ bot AI đến tác tử AI
Bạn có thể đưa ra một trường hợp tốt rằng đây là một bot AI, không phải là một tác tử AI. Nó thực hiện một chiến lược tương đối đơn giản dựa trên thông tin được xác định trước. Chúng ta có thể kích hoạt khả năng tự cải thiện, ví dụ, bằng cách cung cấp một danh sách các pool Uniswap v3 và các giá trị mới nhất của chúng và hỏi sự kết hợp nào có giá trị dự đoán tốt nhất.
Bảo vệ chống trượt giá
Hiện tại không có bảo vệ chống trượt giá (opens in a new tab). Nếu báo giá hiện tại là 2000 đô la và giá dự kiến là 2100 đô la, tác tử sẽ mua. Tuy nhiên, nếu trước khi tác tử mua, chi phí tăng lên 2200 đô la, thì không còn ý nghĩa gì để mua nữa.
Để thực hiện bảo vệ chống trượt giá, hãy chỉ định một giá trị amountOutMinimum trong các dòng 325 và 334 của agent.py (opens in a new tab).
Kết luận
Hy vọng rằng bây giờ bạn đã biết đủ để bắt đầu với các tác tử AI. Đây không phải là một cái nhìn tổng quan toàn diện về chủ đề này; có cả những cuốn sách dành riêng cho điều đó, nhưng điều này là đủ để bạn bắt đầu. Chúc may mắn!
Xem thêm công việc của tôi tại đây (opens in a new tab).
Lần cập nhật trang lần cuối: 10 tháng 2, 2026