イーサリアムで独自のAI取引エージェントを作成する
このチュートリアルでは、簡単なAI取引エージェントの構築方法を学びます。 このエージェントは次の手順で動作します。
- トークンの現在および過去の価格、および関連する可能性のあるその他の情報を読み取ります
- この情報と、その関連性を説明する背景情報を使用してクエリを構築します
- クエリを送信し、予測価格を受け取ります
- 推奨に基づいて取引します
- 待機して繰り返します
このエージェントは、情報を読み取り、それを使用可能な回答をもたらすクエリに変換し、その回答を使用する方法を示します。 これらはすべてAIエージェントに必要な手順です。 このエージェントは、AIで最も一般的に使用される言語であるため、Pythonで実装されています。
なぜこれを行うのか?
自動取引エージェントを使用すると、デベロッパーは取引戦略を選択して実行できます。 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)を含む)で開始するための手順があります
-
まだお持ちでない場合は、Python (opens in a new tab)をダウンロードしてインストールしてください。
-
GitHubリポジトリをクローンします。
git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started cd 260215-ai-agent -
uv(opens in a new tab)をインストールします。 お使いのシステムではコマンドが異なる場合があります。pipx install uv -
ライブラリをダウンロードします。
uv sync -
仮想環境を有効にします。
source .venv/bin/activate -
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
タイムスタンプ、価格、資産(現在は常にWETH/USDC)を持つQuoteオブジェクトのリストを受け取るはずです。
以下に一行ごとの説明を示します。
from web3 import Web3
from web3.contract import Contract
from decimal import Decimal, ROUND_HALF_UP
from dataclasses import dataclass
from datetime import datetime, timezone
from pprint import pprint
import time
import functools
import sys
必要なライブラリをインポートします。 これらは使用されるときに以下で説明されます。
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)で宣伝されているものを使用できます。
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
サイクルのサイズ。 サイクルごとに1回見積もりを確認し、次のサイクルの終わりの値を推定しようとします。
# 読み取っているプールのアドレス
WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")
見積もり値は、アドレス0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 (opens in a new tab)のUniswap 3 USDC/WETHプールから取得されます。 このアドレスは既にチェックサム形式ですが、コードを再利用可能にするためにWeb3.to_checksum_address (opens in a new tab)を使用する方が良いです。
POOL_ABI = [
{ "name": "slot0", ... },
{ "name": "token0", ... },
{ "name": "token1", ... },
]
ERC20_ABI = [
{ "name": "symbol", ... },
{ "name": "decimals", ... }
]
これらは、私たちが連絡する必要のある2つのコントラクトの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)はTrueまたはFalseとして定義され、大文字で始まります。 このデータクラスは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であり、ここで呼び出されたデータクラスのインスタンスです。 ここにはもう1つのパラメータ、ブロック番号があります。
assert block <= w3.eth.block_number, "ブロックは未来にあります"
もし未来を読むことができれば、取引に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)です。 最初の値は、2つのトークン間の為替レートの関数です。
raw_price = (sqrt_price_x96 / Decimal(2**96)) ** 2
オンチェーンでの計算を減らすため、Uniswap v3は実際の為替係数ではなく、その平方根を保存します。 EVMは浮動小数点演算や分数をサポートしていないため、実際の値の代わりに、応答はとなります
# (token0あたりのtoken1)
return 1/(raw_price * self.decimal_factor)
取得する生の価格は、token1ごとに取得できるtoken0の数です。 我々のプールでは、token0はUSDC(米ドルと同じ価値を持つステーブルコイン)であり、token1はWETH (opens in a new tab)です。 私たちが本当に欲しい値は、WETHあたりのドル数であり、その逆数ではありません。
小数係数は、2つのトークンの小数係数 (opens in a new tab)間の比率です。
@dataclass(frozen=True)
class Quote:
timestamp: str
price: Decimal
asset: str
このデータクラスは、特定の時点での特定の資産の価格である見積もりを表します。 この時点では、単一のプールを使用しているため資産も単一であり、assetフィールドは無関係です。 しかし、後でさらに資産を追加します。
def read_token(address: str) -> ERC20Token:
token = w3.eth.contract(address=address, abi=ERC20_ABI)
symbol = token.functions.symbol().call()
decimals = token.functions.decimals().call()
return ERC20Token(
address=address,
symbol=symbol,
decimals=decimals,
contract=token
)
この関数はアドレスを受け取り、そのアドレスにあるトークンコントラクトに関する情報を返します。 新しいWeb3 Contract (opens in a new tab)を作成するには、アドレスとABIをw3.eth.contractに提供します。
def read_pool(address: str) -> PoolInfo:
pool_contract = w3.eth.contract(address=address, abi=POOL_ABI)
token0Address = pool_contract.functions.token0().call()
token1Address = pool_contract.functions.token1().call()
token0 = read_token(token0Address)
token1 = read_token(token1Address)
return PoolInfo(
address=address,
asset=f"{token1.symbol}/{token0.symbol}",
token0=token0,
token1=token1,
contract=pool_contract,
decimal_factor=Decimal(10) ** Decimal(token0.decimals - token1.decimals)
)
この関数は、特定のプール (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)を使用して、値を小数点以下2桁に丸めます。
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リストに追加します。 その後、そのリストを返します。
pool = read_pool(WETHUSDC_ADDRESS)
quotes = get_quotes(
pool,
w3.eth.block_number - 12*CYCLE_BLOCKS,
w3.eth.block_number,
CYCLE_BLOCKS
)
pprint(quotes)
これはスクリプトのメインコードです。 プール情報を読み取り、12の見積もりを取得し、pprint (opens in a new tab)でそれらを表示します。
プロンプトの作成
次に、この見積もりリストをLLMのプロンプトに変換し、予想される将来価値を取得する必要があります。
git checkout 03-create-prompt
uv run agent.py
出力は、次のようなLLMへのプロンプトになります。
以下の見積もりを前提として:
Asset: WETH/USDC
2026-01-20T16:34 3016.21
.
.
.
2026-02-01T17:49 2299.10
Asset: WBTC/WETH
2026-01-20T16:34 29.84
.
.
.
2026-02-01T17:50 33.46
2026-02-02T17:56の時点でWETH/USDCの値はいくらになると予想しますか?
回答は、他のテキストを含まず、小数点以下2桁に丸めた単一の数値として提供してください。
ここには、WETH/USDCとWBTC/WETHの2つの資産の見積もりがあることに注意してください。 別の資産からの見積もりを追加すると、予測精度が向上する可能性があります。
プロンプトの構成
このプロンプトには、LLMプロンプトで非常に一般的な3つのセクションが含まれています。
-
情報。 LLMはトレーニングから多くの情報を持っていますが、通常は最新の情報を持っていません。 これが、ここで最新の見積もりを取得する必要がある理由です。 プロンプトに情報を追加することは、検索拡張生成(RAG) (opens in a new tab)と呼ばれます。
-
実際の質問。 これが私たちが知りたいことです。
-
出力フォーマットの指示。 通常、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")
読み取る必要のある2つのプールがあります。
@dataclass(frozen=True)
class PoolInfo:
.
.
.
reverse: bool = False
def get_price(self, block: int) -> Decimal:
assert block <= w3.eth.block_number, "Block is in the future"
sqrt_price_x96 = Decimal(self.contract.functions.slot0().call(block_identifier=block)[0])
raw_price = (sqrt_price_x96 / Decimal(2**96)) ** 2 # (token1 per token0)
if self.reverse:
return 1/(raw_price * self.decimal_factor)
else:
return raw_price * self.decimal_factor
WETH/USDCプールでは、token1(WETH)を1つ購入するために必要なtoken0(USDC)の数を知りたいです。 WETH/WBTCプールでは、token0(WBTC、ラップされたビットコイン)を1つ購入するために必要なtoken1(WETH)の数を知りたいです。 プールの比率を逆にする必要があるかどうかを追跡する必要があります。
def read_pool(address: str, reverse: bool = False) -> PoolInfo:
.
.
.
return PoolInfo(
.
.
.
asset= f"{token1.symbol}/{token0.symbol}" if reverse else f"{token0.symbol}/{token1.symbol}",
reverse=reverse
)
プールを逆にする必要があるかどうかを知るには、それを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.
"""
プロンプトの残りの部分は期待通りです。
wethusdc_pool = read_pool(WETHUSDC_ADDRESS, True)
wethusdc_quotes = get_quotes(
wethusdc_pool,
w3.eth.block_number - 12*CYCLE_BLOCKS,
w3.eth.block_number,
CYCLE_BLOCKS,
)
wethwbtc_pool = read_pool(WETHWBTC_ADDRESS)
wethwbtc_quotes = get_quotes(
wethwbtc_pool,
w3.eth.block_number - 12*CYCLE_BLOCKS,
w3.eth.block_number,
CYCLE_BLOCKS
)
2つのプールを確認し、両方から見積もりを取得します。
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を使用して作成したため、異なるプロバイダを使用する場合は調整する必要があります。
-
アカウントに資金を供給する (opens in a new tab)—執筆時点での最低額は5ドルです
-
コマンドラインで、プログラムが使用できるようにAPIキーをエクスポートします
export OPENAI_API_KEY=sk-<the rest of the key goes here> -
エージェントをチェックアウトして実行する
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)を呼び出して応答を作成します。
expected_price = Decimal(response.choices[0].message.content.strip())
current_price = wethusdc_quotes[-1].price
print ("Current price:", wethusdc_quotes[-1].price)
print(f"In {future_time}, expected price: {expected_price} USD")
if (expected_price > current_price):
print(f"Buy, I expect the price to go up by {expected_price - current_price} USD")
else:
print(f"Sell, I expect the price to go down by {current_price - expected_price} USD")
価格を出力し、買いまたは売りの推奨を提供します。
予測のテスト
予測を生成できるようになったので、履歴データを使用して、有用な予測を生成できるかどうかを評価することもできます。
uv run test-predictor.py
期待される結果は次のようになります。
2026-01-05T19:50の予測: 予測値 3138.93 USD、実績値 3218.92 USD、誤差 79.99 USD
2026-01-06T19:56の予測: 予測値 3243.39 USD、実績値 3221.08 USD、誤差 22.31 USD
2026-01-07T20:02の予測: 予測値 3223.24 USD、実績値 3146.89 USD、誤差 76.35 USD
2026-01-08T20:11の予測: 予測値 3150.47 USD、実績値 3092.04 USD、誤差 58.43 USD
.
.
.
2026-01-31T22:33の予測: 予測値 2637.73 USD、実績値 2417.77 USD、誤差 219.96 USD
2026-02-01T22:41の予測: 予測値 2381.70 USD、実績値 2318.84 USD、誤差 62.86 USD
2026-02-02T22:49の予測: 予測値 2234.91 USD、実績値 2349.28 USD、誤差 114.37 USD
29回の予測における平均予測誤差: 83.87103448275862068965517241 USD
推奨あたりの平均変化: 4.787931034482758620689655172 USD
変化の標準分散: 104.42 USD
利益が出た日数: 51.72%
損失が出た日数: 48.28%
テスターのほとんどはエージェントと同じですが、ここでは新規または変更された部分を示します。
CYCLES_FOR_TEST = 40 # バックテストでは、テストするサイクル数
# 大量の見積もりを取得
wethusdc_pool = read_pool(WETHUSDC_ADDRESS, True)
wethusdc_quotes = get_quotes(
wethusdc_pool,
w3.eth.block_number - CYCLE_BLOCKS*CYCLES_FOR_TEST,
w3.eth.block_number,
CYCLE_BLOCKS,
)
wethwbtc_pool = read_pool(WETHWBTC_ADDRESS)
wethwbtc_quotes = get_quotes(
wethwbtc_pool,
w3.eth.block_number - CYCLE_BLOCKS*CYCLES_FOR_TEST,
w3.eth.block_number,
CYCLE_BLOCKS
)
CYCLES_FOR_TEST(ここでは40として指定)日分遡ります。
# 予測を作成し、実際の履歴と照合する
total_error = Decimal(0)
changes = []
私たちが関心を持つ誤差には2つのタイプがあります。 1つ目のtotal_errorは、予測子が犯した誤差の単純な合計です。
2つ目の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については、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)を使用して、利益が出た日数と損失が出た日数を数えます。 結果はフィルターオブジェクトであり、長さを取得するにはリストに変換する必要があります。
トランザクションの送信
次に、実際にトランザクションを送信する必要があります。 しかし、システムが証明される前のこの時点では、実際のお金を使いたくありません。 代わりに、メインネットのローカルフォークを作成し、そのネットワークで「取引」します。
ローカルフォークを作成し、取引を有効にする手順は次のとおりです。
-
Foundry (opens in a new tab)をインストールする
-
anvil(opens in a new tab)を起動するanvil --fork-url https://eth.drpc.org --block-time 12anvilはFoundryのデフォルトURLであるhttp://localhost:8545でリッスンしているため、ブロックチェーンを操作するために使用する[`cast`コマンド (opens in a new tab)](https://getfoundry.sh/cast/overview)のURLを指定する必要はありません。 (opens in a new tab) -
anvilで実行すると、ETHを持つ10個のテストアカウントがあります—最初の1つの環境変数を設定しますPRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 ADDRESS=`cast wallet address $PRIVATE_KEY` -
これらは使用する必要のあるコントラクトです。
SwapRouter(opens in a new tab)は、実際に取引に使用するUniswap v3コントラクトです。 プールを通じて直接取引することもできますが、こちらの方がはるかに簡単です。下の2つの変数は、WETHとUSDCの間でスワップするために必要なUniswap v3のパスです。
WETH_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C05861564 WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 -
各テストアカウントには10,000 ETHがあります。 WETHコントラクトを使用して1000 ETHをラップし、取引のために1000 WETHを取得します。
cast send $WETH_ADDRESS "deposit()" --value 1000ether --private-key $PRIVATE_KEY -
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_KEYapprove呼び出しは、SwapRouterが私たちのトークンの一部を使うことを許可するアローワンスを作成します。 コントラクトはイベントを監視できないため、トークンをSwapRouterコントラクトに直接転送しても、支払われたことを認識しません。 代わりに、SwapRouterコントラクトが一定額を使用することを許可し、その後SwapRouterがそれを行います。 これはSwapRouterによって呼び出される関数を介して行われるため、成功したかどうかを知ることができます。 -
両方のトークンが十分にあることを確認します。
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
出力は次のようになります。
(ai-trading-agent) qbzzt@Ori-Cloudnomics:~/260215-ai-agent$ uv run agent.py
現在価格: 1843.16
2026-02-06T23:07時点での予測価格: 1724.41 USD
取引前の勘定残高:
USDC残高: 927301.578272
WETH残高: 500
売り、価格が118.75 USD下落すると予想します
承認トランザクション送信済み: 74e367ddbb407c1aaf567d87aa5863049991b1d2aa092b6b85195d925e2bd41f
承認トランザクションがマイニングされました。
売りトランザクション送信済み: fad1bcf938585c9e90364b26ac7a80eea9efd34c37e5db81e58d7655bcae28bf
売りトランザクションがマイニングされました。
取引後の勘定残高:
USDC残高: 929143.797116
WETH残高: 499
実際に使用するには、いくつかの小さな変更が必要です。
- 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),
}
トランザクションパラメータ。 ノンス (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とは対照的に、購入パラメータは変更される可能性があります。 入力額は、quoteで利用可能な1 WETHのコストです。
def buy(quote: Quote):
buy_params = make_buy_params(quote)
approve_token(wethusdc_pool.token0.contract, buy_params["amountIn"])
txn = swap_router.functions.exactInput(buy_params).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)
print(f"Buy transaction sent: {tx_hash.hex()}")
w3.eth.wait_for_transaction_receipt(tx_hash)
print("Buy transaction mined.")
def sell():
approve_token(wethusdc_pool.token1.contract,
WETH_TRADE_AMOUNT * 10**wethusdc_pool.token1.decimals)
txn = swap_router.functions.exactInput(SELL_PARAMS).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)
print(f"Sell transaction sent: {tx_hash.hex()}")
w3.eth.wait_for_transaction_receipt(tx_hash)
print("Sell transaction mined.")
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)}")
両方の通貨でのユーザー残高を報告します。
print("取引前の勘定残高:")
balances()
if (expected_price > current_price):
print(f"買い、価格が{expected_price - current_price} USD上昇すると予想します")
buy(wethusdc_quotes[-1])
else:
print(f"売り、価格が{current_price - expected_price} USD下落すると予想します")
sell()
print("取引後の勘定残高:")
balances()
このエージェントは現在一度しか動作しません。 ただし、crontab (opens in a new tab)から実行するか、368〜400行をループでラップし、time.sleep (opens in a new tab)を使用して次のサイクルまで待機することで、継続的に動作するように変更できます。
改善の可能性
これは完全な製品版ではありません。基本を教えるための単なる例です。 改善のためのいくつかのアイデアを以下に示します。
よりスマートな取引
エージェントが何をするかを決定する際に無視している2つの重要な事実があります。
- 予想される変化の大きさ。 エージェントは、下落の大きさに関係なく、価格が下落すると予想される場合に固定量の
WETHを売却します。 おそらく、軽微な変化は無視し、価格がどれだけ下落すると予想されるかに基づいて売却する方が良いでしょう。 - 現在のポートフォリオ。 ポートフォリオの10%がWETHで、価格が上がると考えている場合、さらに購入するのは理にかなっています。 しかし、ポートフォリオの90%がWETHである場合、十分にエクスポージャーがあり、さらに購入する必要はありません。 価格が下がると予想される場合は、その逆が当てはまります。
取引戦略を秘密にしたい場合はどうしますか?
AIベンダーは、あなたが彼らのLLMに送信するクエリを見ることができ、これによりあなたがエージェントで開発した天才的な取引システムが公開される可能性があります。 あまりにも多くの人が使用する取引システムは、あなたが買いたいときに多くの人が買おうとし(価格が上がる)、売りたいときに多くの人が売ろうとする(価格が下がる)ため、価値がありません。
この問題を回避するために、例えばLM-Studio (opens in a new tab)を使用して、LLMをローカルで実行できます。
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年3月3日