跳至主要内容

在以太坊上打造你自己的 AI 交易代理

AI
交易
代理
python
中等
Ori Pomerantz
2026年2月13日
34 分鐘閱讀

在本教學中,你將學會如何建立一個簡單的 AI 交易代理。 此代理的運作方式包含以下步驟:

  1. 讀取代幣的當前和過去價格,以及其他潛在的相關資訊
  2. 使用此資訊建立查詢,並附上背景資訊以解釋其可能存在的關聯性
  3. 提交查詢並接收回傳的預測價格
  4. 根據建議進行交易
  5. 等待並重複

此代理示範了如何讀取資訊、將其轉譯為能產生可用答案的查詢,並使用該答案。 這些都是 AI 代理所需的步驟。 此代理以 Python 實作,因為它是 AI 領域中最常用的語言。

為何要這麼做?

自動化交易代理讓開發者可以選擇並執行交易策略。 AI 代理可實現更複雜且動態的交易策略,可能會使用到開發者甚至從未考慮過的資訊和演算法。

工具

本教學使用 Python (opens in a new tab)Web3 函式庫 (opens in a new tab)Uniswap v3 (opens in a new tab) 來進行報價和交易。

為何選擇 Python?

AI 領域中最廣泛使用的語言是 Python (opens in a new tab),所以我們在這裡使用它。 如果你不懂 Python,不用擔心。 這種語言非常清晰,我會詳細解釋它的功能。

Web3 函式庫 (opens in a new tab) 是最常見的 Python 以太坊應用程式介面 (API)。 它相當容易使用。

在區塊鏈上交易

許多去中心化交易所 (DEX) 讓你在以太坊上交易代幣。 然而,由於套利的關係,它們的匯率往往相似。

Uniswap (opens in a new tab) 是一個廣泛使用的去中心化交易所 (DEX),我們可以用它來進行報價(查看代幣的相對價值)和交易。

OpenAI

在大型語言模型方面,我選擇從 OpenAI (opens in a new tab) 開始。 要執行本教學中的應用程式,你需要付費取得 API 存取權限。 最低付款金額 5 美元已綽綽有餘。

開發,按部就班

為了簡化開發過程,我們分階段進行。 每個步驟都是 GitHub 中的一個分支。

入門

以下是在 UNIX 或 Linux(包含 WSL (opens in a new tab))下開始的步驟

  1. 如果你還沒有,請下載並安裝 Python (opens in a new tab)

  2. 複製 GitHub 存放庫。

    1git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started
    2cd 260215-ai-agent
  3. 安裝 uv (opens in a new tab)。 你系統上的指令可能會有所不同。

    1pipx install uv
  4. 下載函式庫。

    1uv sync
  5. 啟動虛擬環境。

    1source .venv/bin/activate
  6. 為了驗證 Python 和 Web3 是否正常運作,請執行 python3 並提供這個程式。 你可以在 >>> 提示符號後輸入;不需要建立檔案。

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

從區塊鏈讀取

下一步是從區塊鏈讀取資料。 要做到這點,你需要切換到 02-read-quote 分支,然後使用 uv 執行程式。

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

你應該會收到一個 Quote 物件的列表,每個物件都包含一個時間戳、一個價格和資產(目前固定為 WETH/USDC)。

以下是逐行解釋。

1from web3 import Web3
2from web3.contract import Contract
3from decimal import Decimal, ROUND_HALF_UP
4from dataclasses import dataclass
5from datetime import datetime, timezone
6from pprint import pprint
7import time
8import functools
9import sys
顯示全部

匯入我們需要的庫。 使用到它們時,會在下面解釋。

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

將 Python 的 print 取代為總是立即清空輸出緩衝區的版本。 這在長時間運行的腳本中很有用,因為我們不想等待狀態更新或除錯輸出。

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

一個用來連線到主網的 URL。 你可以從節點即服務取得,或使用 Chainlist (opens in a new tab) 上所宣傳的其中一個。

1BLOCK_TIME_SECONDS = 12
2MINUTE_BLOCKS = int(60 / BLOCK_TIME_SECONDS)
3HOUR_BLOCKS = MINUTE_BLOCKS * 60
4DAY_BLOCKS = HOUR_BLOCKS * 24

以太坊主網區塊通常每 12 秒產生一個,所以這些是我們預期在一段時間內會產生的區塊數量。 請注意,這不是一個精確的數字。 當區塊提議者離線時,該區塊會被跳過,而下一個區塊的時間將是 24 秒。 如果我們想為某個時間戳取得確切的區塊,我們會使用二元搜尋法 (opens in a new tab)。 然而,這對我們的目的來說已經足夠接近了。 預測未來並不是一門精確的科學。

1CYCLE_BLOCKS = DAY_BLOCKS

週期的規模。 我們每個週期會審視一次報價,並嘗試估計下一個週期結束時的價值。

1# 我們正在讀取的資金池地址
2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")

報價價值取自 Uniswap 3 USDC/WETH 資金池,地址為 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 (opens in a new tab)。 這個地址已經是總和檢查碼格式,但最好使用 Web3.to_checksum_address (opens in a new tab) 來讓程式碼可重複使用。

1POOL_ABI = [
2 { "name": "slot0", ... },
3 { "name": "token0", ... },
4 { "name": "token1", ... },
5]
6
7ERC20_ABI = [
8 { "name": "symbol", ... },
9 { "name": "decimals", ... }
10]
顯示全部

這些是我們需要聯繫的兩個合約的應用程式二進位介面 (ABI) (opens in a new tab)。 為了讓程式碼保持簡潔,我們只包含需要呼叫的函式。

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

初始化 Web3 (opens in a new tab) 函式庫並連線到一個以太坊節點。

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

這是在 Python 中建立資料類別的一種方式。 Contract (opens in a new tab) 資料類型用於連線到合約。 注意 (frozen=True)。 在 Python 中,布林值 (opens in a new tab)被定義為 TrueFalse,首字母大寫。 這個資料類別是 frozen(凍結)的,意思是欄位不能被修改。

注意縮排。 與源自 C 的語言 (opens in a new tab)相反,Python 使用縮排來表示區塊。 Python 直譯器知道下一個定義不屬於這個資料類別,因為它的起始縮排與資料類別的欄位不同。

1@dataclass(frozen=True)
2class PoolInfo:
3 address: str
4 token0: ERC20Token
5 token1: ERC20Token
6 contract: Contract
7 asset: str
8 decimal_factor: Decimal = 1

Decimal (opens in a new tab) 類型用於精確處理小數。

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

這是在 Python 中定義函式的方式。 這個定義有縮排,表示它仍然是 PoolInfo 的一部分。

在屬於資料類別的函式中,第一個參數總是 self,也就是呼叫它的資料類別實例。 這裡還有另一個參數,也就是區塊號碼。

1 assert block <= w3.eth.block_number, "區塊在未來"

如果我們能預知未來,我們就不需要用 AI 來交易了。

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

從 Web3 呼叫以太坊虛擬機 (EVM) 上函式的語法是:<contract object>.functions.<function name>``().call(<parameters>)。 參數可以是 EVM 函式的參數(如果有的話;這裡沒有)或用於修改區塊鏈行為的具名參數 (opens in a new tab)。 這裡我們使用其中一個,block_identifier,來指定我們希望執行的區塊號碼

結果是這個結構體,以陣列形式呈現 (opens in a new tab)。 第一個值是兩種代幣之間匯率的函式。

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

為了減少鏈上計算,Uniswap v3 不儲存實際的匯率因子,而是儲存其平方根。 因為 EVM 不支援浮點數運算或分數,所以回應值並非實際數值,而是 price&#x22C5296

1 # (每 token0 的 token1 數量)
2 return 1/(raw_price * self.decimal_factor)

我們得到的原始價格是每一個 token1 能換到的 token0 數量。 在我們的資金池中,token0 是 USDC(一種與美元價值相同的穩定幣),而 token1WETH (opens in a new tab)。 我們真正想要的價值是每一 WETH 對應的美元數量,而不是其倒數。

小數位數因子是兩種代幣的小數位數因子 (opens in a new tab)之間的比例。

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

這個資料類別代表一個報價:特定資產在特定時間點的價格。 在這個階段,asset 欄位不重要,因為我們只使用一個資金池,所以只有一種資產。 不過,我們稍後會新增更多資產。

1def read_token(address: str) -> ERC20Token:
2 token = w3.eth.contract(address=address, abi=ERC20_ABI)
3 symbol = token.functions.symbol().call()
4 decimals = token.functions.decimals().call()
5
6 return ERC20Token(
7 address=address,
8 symbol=symbol,
9 decimals=decimals,
10 contract=token
11 )
顯示全部

這個函式接收一個地址,並回傳該地址上代幣合約的資訊。 要建立一個新的 Web3 Contract (opens in a new tab),我們將地址和應用程式二進位介面 (ABI) 提供給 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 )
顯示全部

這個函式回傳關於特定資金池 (opens in a new tab)的所有所需資訊。 f"<string>" 語法是一種格式化字串 (opens in a new tab)

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

取得一個 Quote 物件。 block_number 的預設值是 None(無值)。

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

如果沒有指定區塊號碼,就使用 w3.eth.block_number,也就是最新的區塊號碼。 這是 一個 if 陳述式 (opens in a new tab) 的語法。

看起來似乎直接將預設值設為 w3.eth.block_number 會更好,但這行不通,因為那會是函式定義時的區塊號碼。 在長時間運行的代理中,這會是個問題。

1 block = w3.eth.get_block(block_number)
2 price = pool.get_price(block_number)
3 return Quote(
4 timestamp=datetime.fromtimestamp(block.timestamp, timezone.utc).isoformat(),
5 price=price.quantize(Decimal("0.01")),
6 asset=pool.asset
7 )

使用 datetime 函式庫 (opens in a new tab)將其格式化為人類和大型語言模型 (LLM) 都可讀的格式。 使用 Decimal.quantize (opens in a new tab) 將數值四捨五入至小數點後兩位。

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

在 Python 中,你可以使用 list[<type>] 來定義一個只能包含特定類型的列表 (opens in a new tab)

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

在 Python 中,for 迴圈 (opens in a new tab)通常會疊代一個列表。 要尋找報價的區塊號碼列表來自 range (opens in a new tab)

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

對於每個區塊號碼,取得一個 Quote 物件並將其附加到 quotes 列表中。 然後回傳該列表。

1pool = read_pool(WETHUSDC_ADDRESS)
2quotes = get_quotes(
3 pool,
4 w3.eth.block_number - 12*CYCLE_BLOCKS,
5 w3.eth.block_number,
6 CYCLE_BLOCKS
7)
8
9pprint(quotes)
顯示全部

這是腳本的主要程式碼。 讀取資金池資訊,取得十二個報價,然後用 pprint (opens in a new tab) 將它們印出。

建立提示

接下來,我們需要將這個報價列表轉換為給 LLM 的提示,並取得一個預期的未來價值。

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

現在的輸出將會是一個給 LLM 的提示,類似於:

1給定以下報價:
2資產:WETH/USDC
3 2026-01-20T16:34 3016.21
4 .
5 .
6 .
7 2026-02-01T17:49 2299.10
8
9資產:WBTC/WETH
10 2026-01-20T16:34 29.84
11 .
12 .
13 .
14 2026-02-01T17:50 33.46
15
16
17您預期在 2026-02-02T17:56 時,WETH/USDC 的價值會是多少?
18
19請以單一數字提供您的答案,四捨五入至小數點後兩位,
20不要包含任何其他文字。
顯示全部

請注意,這裡有兩種資產的報價:WETH/USDCWBTC/WETH。 新增另一種資產的報價可能會提高預測的準確性。

提示的樣貌

這個提示包含三個部分,這在 LLM 提示中相當常見。

  1. 資訊。 LLM 從訓練中獲得了大量資訊,但通常不是最新的。 這就是我們需要在此處擷取最新報價的原因。 在提示中新增資訊的作法稱為檢索增強生成 (RAG) (opens in a new tab)

  2. 實際問題。 這是我們想知道的。

  3. 輸出格式說明。 通常,LLM 會給我們一個估計值,並解釋它是如何得出的。 這對人類來說更好,但電腦程式只需要最終結果。

程式碼說明

以下是新的程式碼。

1from datetime import datetime, timezone, timedelta

我們需要提供 LLM 我們想要估計的時間。 要取得未來「n 分鐘/小時/天」的時間,我們使用 timedelta 類別 (opens in a new tab)

1# 我們正在讀取的資金池地址
2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")
3WETHWBTC_ADDRESS = Web3.to_checksum_address("0xCBCdF9626bC03E24f779434178A73a0B4bad62eD")

我們需要讀取兩個資金池。

1@dataclass(frozen=True)
2class PoolInfo:
3 .
4 .
5 .
6 reverse: bool = False
7
8 def get_price(self, block: int) -> Decimal:
9 assert block <= w3.eth.block_number, "區塊在未來"
10 sqrt_price_x96 = Decimal(self.contract.functions.slot0().call(block_identifier=block)[0])
11 raw_price = (sqrt_price_x96 / Decimal(2**96)) ** 2 # (每 token0 的 token1 數量)
12 if self.reverse:
13 return 1/(raw_price * self.decimal_factor)
14 else:
15 return raw_price * self.decimal_factor
顯示全部

在 WETH/USDC 資金池中,我們想知道需要多少 token0 (USDC) 才能購買一個 token1 (WETH)。 在 WETH/WBTC 資金池中,我們想知道需要多少 token1 (WETH) 才能購買一個 token0 (WBTC,即包裝比特幣)。 我們需要追蹤資金池的比率是否需要反轉。

1def read_pool(address: str, reverse: bool = False) -> PoolInfo:
2 .
3 .
4 .
5
6 return PoolInfo(
7 .
8 .
9 .
10
11 asset= f"{token1.symbol}/{token0.symbol}" if reverse else f"{token0.symbol}/{token1.symbol}",
12 reverse=reverse
13 )
顯示全部

要知道一個資金池是否需要反轉,我們需要將其作為輸入傳遞給 read_pool。 此外,資產符號也需要正確設定。

語法 <a> if <b> else <c> 是 Python 中相當於三元條件運算子 (opens in a new tab)的寫法,在源自 C 的語言中會是 <b> ? <a> : <c>

1def format_quotes(quotes: list[Quote]) -> str:
2 result = f"Asset: {quotes[0].asset}\n"
3 for quote in quotes:
4 result += f"\t{quote.timestamp[0:16]} {quote.price.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)}\n"
5 return result

這個函式建立一個格式化 Quote 物件列表的字串,假設它們都適用於同一個資產。

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

在 Python 中,多行字串文字 (opens in a new tab)寫成 """ ...。 """

1給定以下報價:
2{
3 functools.reduce(lambda acc, q: acc + '\n' + q,
4 map(lambda q: format_quotes(q), quotes))
5}

在這裡,我們使用 MapReduce (opens in a new tab) 模式,用 format_quotes 為每個報價列表產生一個字串,然後將它們縮減成一個單一字串,以便在提示中使用。

1您預期在 {expected_time} 時,{asset} 的價值會是多少?
2
3請以單一數字提供您的答案,四捨五入至小數點後兩位,
4不要包含任何其他文字。
5 """

提示的其餘部分如預期。

1wethusdc_pool = read_pool(WETHUSDC_ADDRESS, True)
2wethusdc_quotes = get_quotes(
3 wethusdc_pool,
4 w3.eth.block_number - 12*CYCLE_BLOCKS,
5 w3.eth.block_number,
6 CYCLE_BLOCKS,
7)
8
9wethwbtc_pool = read_pool(WETHWBTC_ADDRESS)
10wethwbtc_quotes = get_quotes(
11 wethwbtc_pool,
12 w3.eth.block_number - 12*CYCLE_BLOCKS,
13 w3.eth.block_number,
14 CYCLE_BLOCKS
15)
顯示全部

檢查這兩個資金池並從兩者取得報價。

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

確定我們想要估計的未來時間點,並建立提示。

與 LLM 介接

接下來,我們提示一個實際的 LLM 並接收一個預期的未來價值。 我用 OpenAI 編寫了這個程式,所以如果你想使用不同的供應商,你需要進行調整。

  1. 取得一個 OpenAI 帳戶 (opens in a new tab)

  2. 為帳戶儲值 (opens in a new tab) — 撰寫本文時的最低金額為 5 美元

  3. 建立一個 API 金鑰 (opens in a new tab)

  4. 在命令列中,匯出 API 金鑰,以便你的程式可以使用它

    1export OPENAI_API_KEY=sk-<金鑰的其餘部分放在這裡>
  5. 簽出並執行代理

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

以下是新的程式碼。

1from openai import OpenAI
2
3open_ai = OpenAI() # 用戶端會讀取 OPENAI_API_KEY 環境變數

匯入並實例化 OpenAI API。

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)

呼叫 OpenAI API (open_ai.chat.completions.create) 來建立回應。

1expected_price = Decimal(response.choices[0].message.content.strip())
2current_price = wethusdc_quotes[-1].price
3
4print ("目前價格:", wethusdc_quotes[-1].price)
5print(f"在 {future_time},預期價格:{expected_price} USD")
6
7if (expected_price > current_price):
8 print(f"買入,我預期價格會上漲 {expected_price - current_price} USD")
9else:
10 print(f"賣出,我預期價格會下跌 {current_price - expected_price} USD")
顯示全部

輸出價格並提供買入或賣出建議。

測試預測

既然我們能產生預測,我們也可以使用歷史資料來評估我們是否產生了有用的預測。

1uv run test-predictor.py

預期結果類似於:

12026-01-05T19:50 的預測:預測 3138.93 USD,實際 3218.92 USD,誤差 79.99 USD
22026-01-06T19:56 的預測:預測 3243.39 USD,實際 3221.08 USD,誤差 22.31 USD
32026-01-07T20:02 的預測:預測 3223.24 USD,實際 3146.89 USD,誤差 76.35 USD
42026-01-08T20:11 的預測:預測 3150.47 USD,實際 3092.04 USD,誤差 58.43 USD
5.
6.
7.
82026-01-31T22:33 的預測:預測 2637.73 USD,實際 2417.77 USD,誤差 219.96 USD
92026-02-01T22:41 的預測:預測 2381.70 USD,實際 2318.84 USD,誤差 62.86 USD
102026-02-02T22:49 的預測:預測 2234.91 USD,實際 2349.28 USD,誤差 114.37 USD
1129 次預測的平均預測誤差:83.87103448275862068965517241 USD
12每次建議的平均變動:4.787931034482758620689655172 USD
13變動的標準差:104.42 USD
14獲利天數:51.72%
15虧損天數:48.28%
顯示全部

測試器的大部分內容與代理相同,但以下是新增或修改的部分。

1CYCLES_FOR_TEST = 40 # 針對回測,我們測試多少個週期
2
3# 取得大量報價
4wethusdc_pool = read_pool(WETHUSDC_ADDRESS, True)
5wethusdc_quotes = get_quotes(
6 wethusdc_pool,
7 w3.eth.block_number - CYCLE_BLOCKS*CYCLES_FOR_TEST,
8 w3.eth.block_number,
9 CYCLE_BLOCKS,
10)
11
12wethwbtc_pool = read_pool(WETHWBTC_ADDRESS)
13wethwbtc_quotes = get_quotes(
14 wethwbtc_pool,
15 w3.eth.block_number - CYCLE_BLOCKS*CYCLES_FOR_TEST,
16 w3.eth.block_number,
17 CYCLE_BLOCKS
18)
顯示全部

我們回溯 CYCLES_FOR_TEST(此處指定為 40)天。

1# 建立預測並與真實歷史進行比對
2
3total_error = Decimal(0)
4changes = []

我們感興趣的有兩種類型的錯誤。 第一種 total_error,是預測器所犯錯誤的總和。

要理解第二種 changes,我們需要記住代理的目的。 它的目的不是預測 WETH/USDC 的比率(ETH 價格)。 而是發出賣出和買入的建議。 如果目前價格是 2000 美元,而它預測明天是 2010 美元,我們不介意實際結果是 2020 美元,我們還能多賺錢。 但我們_確實_介意如果它預測了 2010 美元,並根據該建議購買了 ETH,結果價格卻跌到 1990 美元。

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

我們只能檢視那些有完整歷史紀錄的案例(用於預測的數值和與之比較的真實世界數值)。 這表示最新的案例必須是從 CYCLES_BACK 之前開始的那個。

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

使用切片 (slices) (opens in a new tab) 來取得與代理使用的樣本數量相同的樣本。 從這裡到下一個段落之間的程式碼,與我們在代理中用來取得預測的程式碼相同。

1 predicted_price = Decimal(response.choices[0].message.content.strip())
2 real_price = wethusdc_quotes[index+CYCLES_BACK].price
3 prediction_time_price = wethusdc_quotes[index+CYCLES_BACK-1].price

取得預測價格、實際價格,以及預測當時的價格。 我們需要預測當時的價格,以判斷建議是買入還是賣出。

1 error = abs(predicted_price - real_price)
2 total_error += error
3 print (f"針對 {prediction_time} 的預測:預測 {predicted_price} USD,實際 {real_price} USD,誤差 {error} USD")

計算誤差,並將其加到總和中。

1 recomended_action = 'buy' if predicted_price > prediction_time_price else 'sell'
2 price_increase = real_price - prediction_time_price
3 changes.append(price_increase if recomended_action == 'buy' else -price_increase)

對於 changes,我們想要知道買入或賣出一個 ETH 的金錢影響。 所以首先,我們需要確定建議,然後評估實際價格的變化,以及該建議是賺錢(正向變動)還是虧錢(負向變動)。

1print (f"在 {len(wethusdc_quotes)-CYCLES_BACK} 次預測中的平均預測誤差:{total_error / Decimal(len(wethusdc_quotes)-CYCLES_BACK)} USD")
2
3length_changes = Decimal(len(changes))
4mean_change = sum(changes, Decimal(0)) / length_changes
5print (f"每次建議的平均變動:{mean_change} USD")
6var = sum((x - mean_change) ** 2 for x in changes) / length_changes
7print (f"變動的標準差:{var.sqrt().quantize(Decimal("0.01"))} USD")

回報結果。

1print (f"獲利天數:{len(list(filter(lambda x: x > 0, changes)))/length_changes:.2%}")
2print (f"虧損天數:{len(list(filter(lambda x: x < 0, changes)))/length_changes:.2%}")

使用 filter (opens in a new tab) 來計算獲利天數和虧損天數。 結果是一個 filter 物件,我們需要將它轉換成列表才能取得長度。

提交交易

現在我們需要實際提交交易。 然而,在系統被證實有效之前,我不想在此時花費真金白銀。 取而代之,我們將建立一個主網的本地分叉,並在該網路上進行「交易」。

以下是建立本地分叉並啟用交易的步驟。

  1. 安裝 Foundry (opens in a new tab)

  2. 啟動 anvil (opens in a new tab)

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

    anvil 正在 Foundry 的預設 URL (http://localhost:8545 (opens in a new tab)) 上監聽,所以我們不需要為我們用來操作區塊鏈的 cast 指令 (opens in a new tab)指定 URL。

  3. anvil 中運行時,有十個擁有 ETH 的測試帳戶 — 為第一個帳戶設定環境變數

    1PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    2ADDRESS=`cast wallet address $PRIVATE_KEY`
  4. 這些是我們需要使用的合約。 SwapRouter (opens in a new tab) 是我們用來實際進行交易的 Uniswap v3 合約。 我們可以透過資金池直接交易,但這樣做要簡單得多。

    下面兩個變數是 WETH 和 USDC 之間進行交換所需的 Uniswap v3 路徑。

    1WETH_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
    2USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    3POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640
    4SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C05861564
    5WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    6USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
  5. 每個測試帳戶都有 10,000 ETH。 使用 WETH 合約來包裝 1000 ETH,以取得 1000 WETH 用於交易。

    1cast send $WETH_ADDRESS "deposit()" --value 1000ether --private-key $PRIVATE_KEY
  6. 使用 SwapRouter 將 500 WETH 交易為 USDC。

    1cast send $WETH_ADDRESS "approve(address,uint256)" $SWAP_ROUTER 500ether --private-key $PRIVATE_KEY
    2MAXINT=`cast max-int uint256`
    3cast send $SWAP_ROUTER \
    4 "exactInput((bytes,address,uint256,uint256,uint256))" \
    5 "($WETH_TO_USDC,$ADDRESS,$MAXINT,500ether,1000000)" \
    6 --private-key $PRIVATE_KEY

    approve 呼叫會建立一個額度,允許 SwapRouter 花費我們的一些代幣。 合約無法監控事件,所以如果我們直接將代幣轉移到 SwapRouter 合約,它不會知道自己收到了款項。 取而代之,我們允許 SwapRouter 合約花費一定金額,然後由 SwapRouter 執行此操作。 這是透過 SwapRouter 呼叫的一個函式來完成的,所以它知道是否成功。

  7. 確認你擁有足夠的兩種代幣。

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

既然我們有了 WETH 和 USDC,我們就可以實際運行代理了。

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

輸出將類似於:

1(ai-trading-agent) qbzzt@Ori-Cloudnomics:~/260215-ai-agent$ uv run agent.py
2目前價格:1843.16
3在 2026-02-06T23:07,預期價格:1724.41 USD
4交易前帳戶餘額:
5USDC 餘額:927301.578272
6WETH 餘額:500
7賣出,我預期價格會下跌 118.75 USD
8授權交易已發送:74e367ddbb407c1aaf567d87aa5863049991b1d2aa092b6b85195d925e2bd41f
9授權交易已上鏈。
10賣出交易已發送:fad1bcf938585c9e90364b26ac7a80eea9efd34c37e5db81e58d7655bcae28bf
11賣出交易已上鏈。
12交易後帳戶餘額:
13USDC 餘額:929143.797116
14WETH 餘額:499
顯示全部

要實際使用它,你需要做一些小小的改動。

  • 在第 14 行,將 MAINNET_URL 更改為一個真實的存取點,例如 https://eth.drpc.org
  • 在第 28 行,將 PRIVATE_KEY 更改為你自己的私密金鑰
  • 除非你非常富有,並且可以每天為一個未經證實的代理買入或賣出 1 個 ETH,否則你可能需要更改第 29 行以減少 WETH_TRADE_AMOUNT

程式碼說明

以下是新的程式碼。

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

與我們在步驟 4 中使用的變數相同。

1WETH_TRADE_AMOUNT=1

交易的數量。

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

要實際進行交易,我們需要 approve 函式。 我們也想顯示交易前後的餘額,所以我們還需要 balanceOf

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

SwapRouter ABI 中,我們只需要 exactInput。 還有一個相關的函式 exactOutput,我們可以用它來剛好買入一個 WETH,但為求簡單,我們在這兩種情況下都只使用 exactInput

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

account (opens in a new tab)SwapRouter 合約的 Web3 定義。

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 }

交易參數。 我們在這裡需要一個函式,因為隨機數 (nonce) (opens in a new tab) 每次都必須改變。

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

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)

這就是我們在 Web3 中發送交易的方式。 首先我們使用Contract 物件 (opens in a new tab)來建構交易。 然後我們使用 web3.eth.account.sign_transaction (opens in a new tab) 來簽署交易,使用 PRIVATE_KEY。 最後,我們使用 w3.eth.send_raw_transaction (opens in a new tab) 來發送交易。

1 print(f"授權交易已發送:{tx_hash.hex()}")
2 w3.eth.wait_for_transaction_receipt(tx_hash)
3 print("授權交易已上鏈。")

w3.eth.wait_for_transaction_receipt (opens in a new tab) 會等到交易被挖出。 如果需要,它會回傳收據。

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}

這些是賣出 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 }

SELL_PARAMS 不同,買入參數是可以變動的。 輸入金額是 1 WETH 的成本,可在 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"買入交易已發送:{tx_hash.hex()}")
8 w3.eth.wait_for_transaction_receipt(tx_hash)
9 print("買入交易已上鏈。")
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"賣出交易已發送:{tx_hash.hex()}")
19 w3.eth.wait_for_transaction_receipt(tx_hash)
20 print("賣出交易已上鏈。")
顯示全部

buy()sell() 函式幾乎完全相同。 首先我們為 SwapRouter 授權一個足夠的額度,然後用正確的路徑和金額呼叫它。

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

回報使用者兩種貨幣的餘額。

1print("交易前帳戶餘額:")
2balances()
3
4if (expected_price > current_price):
5 print(f"買入,我預期價格會上漲 {expected_price - current_price} USD")
6 buy(wethusdc_quotes[-1])
7else:
8 print(f"賣出,我預期價格會下跌 {current_price - expected_price} USD")
9 sell()
10
11print("交易後帳戶餘額:")
12balances()
顯示全部

這個代理目前只能運作一次。 然而,你可以透過 crontab (opens in a new tab) 運行它,或將第 368-400 行包裝在一個迴圈中並使用 time.sleep (opens in a new tab) 等待下一個週期,使其連續運作。

可能的改進

這不是一個完整的產品版本;它只是一個用來教導基礎知識的範例。 以下是一些改進的建議。

更聰明的交易

代理在決定要做什麼時忽略了兩個重要的事實。

  • 預期變動的幅度。 如果預期價格會下跌,代理會賣出固定數量的 WETH,無論下跌幅度如何。 可以說,忽略微小的變化,並根據我們預期價格下跌的幅度來賣出會更好。
  • 目前的投資組合。 如果你投資組合的 10% 是 WETH,而你認為價格會上漲,那麼購買更多可能是有意義的。 但如果你投資組合的 90% 是 WETH,你可能已經有足夠的曝險,不需要再購買更多。 如果你預期價格會下跌,情況則相反。

如果你想讓你的交易策略保密怎麼辦?

AI 供應商可以看到你發送給他們 LLM 的查詢,這可能會暴露你用代理開發出的天才交易系統。 一個太多人使用的交易系統是沒有價值的,因為太多人會在你想要買入時嘗試買入(導致價格上漲),並在你想要賣出時嘗試賣出(導致價格下跌)。

你可以本地運行一個 LLM,例如使用 LM-Studio (opens in a new tab),來避免這個問題。

從 AI 機器人到 AI 代理

你可以很有力地論證這是一個AI 機器人,而不是 AI 代理。 它實作了一個相對簡單的策略,依賴於預先定義的資訊。 我們可以啟用自我改進,例如,提供一個 Uniswap v3 資金池及其最新價值的列表,並詢問哪種組合具有最佳的預測價值。

滑價保護

目前沒有滑價保護 (opens in a new tab)。 如果目前的報價是 2000 美元,預期價格是 2100 美元,代理將會買入。 然而,如果在代理買入前成本上升到 2200 美元,那麼再買入就沒有意義了。

要實作滑價保護,請在 agent.py (opens in a new tab) 的第 325 行和第 334 行指定一個 amountOutMinimum 值。

結論

希望你現在已經學會足夠的知識來開始使用 AI 代理了。 這並不是對這個主題的全面概述;有專門的書籍來探討這個主題,但這足以讓你入門。 祝您好運!

在此查看我的更多作品 (opens in a new tab)

頁面最後更新時間: 2026年2月10日

這個使用教學對你有幫助嗎?