跳转到主要内容

在以太坊上构建你自己的 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

对于大语言模型,我选择从 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 资金池。该地址已经是校验和形式,但最好使用 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 中创建数据类的一种方法。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,来指定我们希望在其中运行的区块号

结果是这个结构体,以数组形式呈现 (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>] 定义一个只能包含特定类型的列表 (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)它们。

创建提示词

接下来,我们需要将此报价列表转换为 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 资金池中,我们想知道购买一个 token1 (WETH) 需要多少 token0 (USDC)。在 WETH/WBTC 资金池中,我们想知道购买一个 token0 (WBTC,即封装比特币) 需要多少 token1 (WETH)。我们需要跟踪资金池的比率是否需要反转。

要知道资金池是否需要反转,我们将其作为 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 为每个报价列表生成一个字符串,然后将它们归约(reduce)为单个字符串以在提示词中使用。

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. 检出并运行代理

    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]

使用切片 (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,我们想要买入或卖出一个 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) 计算盈利天数和亏损天数。结果是一个过滤器对象,我们需要将其转换为列表以获取长度。

提交交易

现在我们需要实际提交交易。然而,在系统得到验证之前,我不想在此时花费真金白银。相反,我们将创建主网的本地分叉,并在该网络上进行“交易”。

以下是创建本地分叉并启用交易的步骤。

  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,因此我们不需要为用于操作区块链的 (opens in a new tab) cast 命令 (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,我们可以用它来精确购买一个 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),
    }

交易参数。我们在这里需要一个函数,因为随机数 (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)