跳至主要內容

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

AI
交易
代理
Python
中階
奧里·波梅蘭茨
2026年2月13日
33 分鐘閱讀

在本教學中,你將學習如何建立一個簡單的 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) 讓你在以太坊上交易代幣。然而,由於套利的關係,它們的匯率往往很相似。

尤尼斯瓦普 (opens in a new tab) 是一個廣泛使用的 DEX,我們可以用它來獲取報價(查看代幣相對價值)和進行交易。

OpenAI

至於大型語言模型 (LLM),我選擇從 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 儲存庫。

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

    pipx install uv
    
  4. 下載函式庫。

    uv sync
    
  5. 啟動虛擬環境。

    source .venv/bin/activate
    
  6. 若要驗證 Python 和 Web3 是否正常運作,請執行 python3 並提供此程式。你可以在 >>> 提示字元下輸入;不需要建立檔案。

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

從區塊鏈讀取

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

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

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

以下是逐行解釋。

匯入我們需要的函式庫。它們會在下方使用時進行解釋。

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

將 Python 的 print 替換為始終立即刷新輸出的版本。這在長時間執行的腳本中很有用,因為我們不想等待狀態更新或除錯輸出。

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

連接至主網的 URL。你可以從 節點即服務 (Node as a service) 取得一個,或使用 Chainlist (opens in a new tab) 中宣傳的 URL。

BLOCK_TIME_SECONDS = 12
MINUTE_BLOCKS = int(60 / BLOCK_TIME_SECONDS)
HOUR_BLOCKS = MINUTE_BLOCKS * 60
DAY_BLOCKS = HOUR_BLOCKS * 24

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

CYCLE_BLOCKS = DAY_BLOCKS

週期的長度。我們每個週期審查一次報價,並嘗試估計下一個週期結束時的價值。

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

報價取自地址為 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 (opens in a new tab) 的尤尼斯瓦普 3 USDC/WETH 資金池。此地址已採用校驗和 (checksum) 格式,但最好使用 Web3.to_checksum_address (opens in a new tab) 以使程式碼可重複使用。

這些是我們需要互動的兩個合約的 ABI (opens in a new tab)。為了保持程式碼簡潔,我們只包含需要呼叫的函式。

w3 = Web3(Web3.HTTPProvider(MAINNET_URL))

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

@dataclass(frozen=True)
class ERC20Token:
    address: str
    symbol: str
    decimals: int
    contract: Contract

這是 Python 中建立資料類別 (data class) 的一種方式。Contract (opens in a new tab) 資料型別用於連接合約。請注意 (frozen=True)。在 Python 中,布林值 (opens in a new tab)定義為 TrueFalse,首字母大寫。此資料類別是 frozen,這意味著欄位無法修改。

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

@dataclass(frozen=True)
class PoolInfo:
    address: str
    token0: ERC20Token
    token1: ERC20Token
    contract: Contract
    asset: str
    decimal_factor: Decimal = 1

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

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

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

在屬於資料類別的函式中,第一個參數始終是 self,即呼叫此處的資料類別實例。這裡還有另一個參數:區塊號碼。

        assert block <= w3.eth.block_number, "Block is in the future"

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

        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,來指定我們希望在哪個區塊號碼中執行。

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

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

為了減少鏈上計算,尤尼斯瓦普 v3 不儲存實際的兌換係數,而是儲存其平方根。因為 EVM 不支援浮點數運算或分數,所以回應的不是實際值,而是 price296

         # (每個代幣0的代幣1)
        return 1/(raw_price * self.decimal_factor)

我們獲得的原始價格是每個 token1 可以換得的 token0 數量。在我們的資金池中,token0 是 USDC(價值等同於美元的穩定幣),而 token1WETH (opens in a new tab)。我們真正想要的值是每 WETH 多少美元,而不是反過來。

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

@dataclass(frozen=True)
class Quote:
    timestamp: str
    price: Decimal
    asset: str

此資料類別代表一個報價:特定資產在給定時間點的價格。在這一點上,asset 欄位無關緊要,因為我們使用單一資金池,因此只有單一資產。不過,我們稍後會新增更多資產。

此函式接收一個地址,並傳回該地址代幣合約的相關資訊。要建立一個新的 Web3 Contract (opens in a new tab),我們將地址和 ABI 提供給 w3.eth.contract

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

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

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

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

如果未指定區塊號碼,則使用 w3.eth.block_number,即最新的區塊號碼。這是 if 敘述 (opens in a new tab)的語法。

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

    block = w3.eth.get_block(block_number)
    price = pool.get_price(block_number)
    return Quote(
        timestamp=datetime.fromtimestamp(block.timestamp, timezone.utc).isoformat(),
        price=price.quantize(Decimal("0.01")),
        asset=pool.asset
    )

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

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

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

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

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

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

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

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

建立提示詞 (Prompt)

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

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

輸出現在將成為給 LLM 的提示詞,類似於:

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

提示詞的樣貌

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

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

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

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

程式碼解釋

這是新的程式碼。

from datetime import datetime, timezone, timedelta

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

# 我們正在讀取的各個池的地址
WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")
WETHWBTC_ADDRESS = Web3.to_checksum_address("0xCBCdF9626bC03E24f779434178A73a0B4bad62eD")

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

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

要知道資金池是否需要反轉,我們將其作為 read_pool 的輸入。此外,資產代號也需要正確設定。

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

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

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

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

在 Python 中,多行字串常值 (opens in a new tab)寫為 """ .... """

Given these quotes:
{
    functools.reduce(lambda acc, q: acc + '\n' + q,
        map(lambda q: format_quotes(q), quotes))
}

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

What would you expect the value for {asset} to be at time {expected_time}?

Provide your answer as a single number rounded to two decimal places,
without any other text.
    """

提示詞的其餘部分如預期。

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

future_time = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat()[0:16]

print(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 金鑰,以便你的程式可以使用它

    export OPENAI_API_KEY=sk-<the rest of the key goes here>
    
  5. 簽出 (Checkout) 並執行代理

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

這是新的程式碼。

from openai import OpenAI

open_ai = OpenAI()  # 客戶端讀取 OPENAI_API_KEY 環境變數

匯入並實例化 OpenAI API。

response = open_ai.chat.completions.create(
    model="gpt-4-turbo",
    messages=[
        {"role": "user", "content": prompt}
    ],
    temperature=0.0,
    max_tokens=16,
)

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

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

測試預測

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

uv run test-predictor.py

預期結果類似於:

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

我們回顧 CYCLES_FOR_TEST(此處指定為 40)天前的資料。

# 建立預測並與真實歷史進行比對

total_error = Decimal(0)
changes = []

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

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

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

我們只能查看具有完整歷史記錄(用於預測的值以及用於比較的真實世界值)的案例。這意味著最新的案例必須是 CYCLES_BACK 前開始的案例。

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

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

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

取得預測價格、實際價格以及預測時的價格。我們需要預測時的價格來決定建議是買入還是賣出。

    error = abs(predicted_price - real_price)
    total_error += error
    print (f"Prediction for {prediction_time}: predicted {predicted_price} USD, real {real_price} USD, error {error} USD")

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

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

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

print (f"Mean prediction error over {len(wethusdc_quotes)-CYCLES_BACK} predictions: {total_error / Decimal(len(wethusdc_quotes)-CYCLES_BACK)} USD")

length_changes = Decimal(len(changes))
mean_change = sum(changes, Decimal(0)) / length_changes
print (f"Mean change per recommendation: {mean_change} USD")
var = sum((x - mean_change) ** 2 for x in changes) / length_changes
print (f"Standard variance of changes: {var.sqrt().quantize(Decimal("0.01"))} USD")

報告結果。

print (f"Profitable days: {len(list(filter(lambda x: x > 0, changes)))/length_changes:.2%}")
print (f"Losing days: {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)

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

    anvil 正在監聽 Foundry 的預設 URL (http://localhost:8545),因此我們不需要為用來操作區塊鏈的[`cast` (opens in a new tab) 指令](https://getfoundry.sh/cast/overview)指定 (opens in a new tab) URL。

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

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

    底部的兩個變數是在 WETH 和 USDC 之間兌換所需的尤尼斯瓦普 v3 路徑。

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

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

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

    approve 呼叫會建立一個授權額度,允許 SwapRouter 花費我們的一些代幣。合約無法監控事件,因此如果我們直接將代幣轉帳給 SwapRouter 合約,它不會知道自己已收到付款。相反地,我們允許 SwapRouter 合約花費特定金額,然後由 SwapRouter 來執行。這是透過由 SwapRouter 呼叫的函式來完成的,因此它會知道是否成功。

  7. 驗證你擁有足夠的兩種代幣。

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

現在我們有了 WETH 和 USDC,我們可以實際執行代理了。

git checkout 05-trade
uv run agent.py

輸出將類似於:

要實際使用它,你需要進行一些小修改。

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

程式碼解釋

這是新的程式碼。

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

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

WETH_TRADE_AMOUNT=1

交易金額。

ERC20_ABI = [
    { "name": "symbol", ... },
    { "name": "decimals", ... },
    { "name": "balanceOf", ...},
    { "name": "approve", ...}
]

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

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

SwapRouter ABI 中,我們只需要 exactInput。有一個相關的函式 exactOutput,我們可以用它來精確買入 1 WETH,但為了簡單起見,我們在兩種情況下都只使用 exactInput

account = w3.eth.account.from_key(PRIVATE_KEY)
swap_router = w3.eth.contract(
    address=SWAP_ROUTER_ADDRESS,
    abi=SWAP_ROUTER_ABI
)

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

def txn_params() -> dict:
    return {
        "from": account.address,
        "value": 0,
        "gas": 300000,
        "nonce": w3.eth.get_transaction_count(account.address),
    }

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

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

SwapRouter 授權代幣額度。

    txn = contract.functions.approve(SWAP_ROUTER_ADDRESS, amount).build_transaction(txn_params())
    signed_txn = w3.eth.account.sign_transaction(txn, private_key=PRIVATE_KEY)
    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) 來發送交易。

    print(f"Approve transaction sent: {tx_hash.hex()}")
    w3.eth.wait_for_transaction_receipt(tx_hash)
    print("Approve transaction mined.")

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

SELL_PARAMS = {
    "path": WETH_TO_USDC,
    "recipient": account.address,
    "deadline": 2**256 - 1,
    "amountIn": WETH_TRADE_AMOUNT * 10 ** wethusdc_pool.token1.decimals,
    "amountOutMinimum": 0,
}

這些是賣出 WETH 時的參數。

def make_buy_params(quote: Quote) -> dict:
    return {
        "path": USDC_TO_WETH,
        "recipient": account.address,
        "deadline": 2**256 - 1,
        "amountIn": int(quote.price*WETH_TRADE_AMOUNT) * 10**wethusdc_pool.token0.decimals,
        "amountOutMinimum": 0,
    }

SELL_PARAMS 相反,買入參數可能會改變。輸入金額是 1 WETH 的成本,可從 quote 中取得。

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

def balances():
    token0_balance = wethusdc_pool.token0.contract.functions.balanceOf(account.address).call()
    token1_balance = wethusdc_pool.token1.contract.functions.balanceOf(account.address).call()

    print(f"{wethusdc_pool.token0.symbol} Balance: {Decimal(token0_balance) / Decimal(10 ** wethusdc_pool.token0.decimals)}")
    print(f"{wethusdc_pool.token1.symbol} Balance: {Decimal(token1_balance) / Decimal(10 ** wethusdc_pool.token1.decimals)}")

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

此代理目前只會執行一次。不過,你可以將其變更為持續執行,方法是透過 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 代理。它實作了一個相對簡單的策略,依賴於預先定義的資訊。我們可以啟用自我改進功能,例如,提供一個尤尼斯瓦普 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)