Erstellen Sie Ihren eigenen KI-Trading-Agenten auf Ethereum
In diesem Tutorial lernen Sie, wie Sie einen einfachen KI-Trading-Agenten erstellen. Dieser Agent arbeitet nach folgenden Schritten:
- Lesen der aktuellen und vergangenen Preise eines Tokens sowie anderer potenziell relevanter Informationen
- Erstellen einer Abfrage mit diesen Informationen sowie Hintergrundinformationen, um zu erklären, wie sie relevant sein könnten
- Einreichen der Abfrage und Erhalt eines prognostizierten Preises
- Handeln basierend auf der Empfehlung
- Warten und wiederholen
Dieser Agent demonstriert, wie man Informationen liest, sie in eine Abfrage übersetzt, die eine brauchbare Antwort liefert, und diese Antwort nutzt. All dies sind erforderliche Schritte für einen KI-Agenten. Dieser Agent ist in Python implementiert, da dies die am häufigsten in der KI verwendete Sprache ist.
Warum das Ganze?
Automatisierte Trading-Agenten ermöglichen es Entwicklern, eine Trading-Strategie auszuwählen und auszuführen. KI-Agenten ermöglichen komplexere und dynamischere Trading-Strategien, die potenziell Informationen und Algorithmen nutzen, an die der Entwickler noch gar nicht gedacht hat.
Die Werkzeuge
Dieses Tutorial verwendet Python (opens in a new tab), die Web3-Bibliothek (opens in a new tab) und Uniswap v3 (opens in a new tab) für Preisangebote und den Handel.
Warum Python?
Die am weitesten verbreitete Sprache für KI ist Python (opens in a new tab), daher verwenden wir sie hier. Machen Sie sich keine Sorgen, wenn Sie Python nicht kennen. Die Sprache ist sehr übersichtlich, und ich werde genau erklären, was sie tut.
Die Web3-Bibliothek (opens in a new tab) ist die gängigste Python-Ethereum-API. Sie ist ziemlich einfach zu bedienen.
Handeln auf der Blockchain
Es gibt viele dezentrale Börsen (DEX), die es Ihnen ermöglichen, Token auf Ethereum zu handeln. Aufgrund von Arbitrage weisen sie jedoch tendenziell ähnliche Wechselkurse auf.
Uniswap (opens in a new tab) ist eine weit verbreitete DEX, die wir sowohl für Preisangebote (um die relativen Werte von Token zu sehen) als auch für Trades nutzen können.
OpenAI
Für ein großes Sprachmodell (Large Language Model, LLM) habe ich mich für den Einstieg mit OpenAI (opens in a new tab) entschieden. Um die Anwendung in diesem Tutorial auszuführen, müssen Sie für den API-Zugang bezahlen. Die Mindestzahlung von 5 $ ist mehr als ausreichend.
Entwicklung, Schritt für Schritt
Um die Entwicklung zu vereinfachen, gehen wir in Phasen vor. Jeder Schritt ist ein Branch auf GitHub.
Erste Schritte
Hier sind die Schritte für den Einstieg unter UNIX oder Linux (einschließlich WSL (opens in a new tab))
-
Falls Sie es noch nicht haben, laden Sie Python (opens in a new tab) herunter und installieren Sie es.
-
Klonen Sie das GitHub-Repository.
git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started cd 260215-ai-agent -
Installieren Sie
uv(opens in a new tab). Der Befehl auf Ihrem System könnte abweichen.pipx install uv -
Laden Sie die Bibliotheken herunter.
uv sync -
Aktivieren Sie die virtuelle Umgebung.
source .venv/bin/activate -
Um zu überprüfen, ob Python und Web3 korrekt funktionieren, führen Sie
python3aus und übergeben Sie ihm dieses Programm. Sie können es an der>>>-Eingabeaufforderung eingeben; es ist nicht nötig, eine Datei zu erstellen.from web3 import Web3 MAINNET_URL = "https://eth.drpc.org" w3 = Web3(Web3.HTTPProvider(MAINNET_URL)) w3.eth.block_number quit()
Lesen aus der Blockchain
Der nächste Schritt ist das Lesen aus der Blockchain. Dazu müssen Sie in den Branch 02-read-quote wechseln und dann uv verwenden, um das Programm auszuführen.
git checkout 02-read-quote
uv run agent.py
Sie sollten eine Liste von Quote-Objekten erhalten, jedes mit einem Zeitstempel, einem Preis und dem Asset (derzeit immer WETH/USDC).
Hier ist eine zeilenweise Erklärung.
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
Importieren Sie die benötigten Bibliotheken. Sie werden unten erklärt, wenn sie verwendet werden.
print = functools.partial(print, flush=True)
Ersetzt Pythons print durch eine Version, die die Ausgabe immer sofort leert (flush). Dies ist in einem lang laufenden Skript nützlich, da wir nicht auf Statusaktualisierungen oder Debugging-Ausgaben warten möchten.
MAINNET_URL = "https://eth.drpc.org"
Eine URL, um zum Mainnet zu gelangen. Sie können eine von einem Node-as-a-Service-Anbieter erhalten oder eine der in Chainlist (opens in a new tab) beworbenen nutzen.
BLOCK_TIME_SECONDS = 12
MINUTE_BLOCKS = int(60 / BLOCK_TIME_SECONDS)
HOUR_BLOCKS = MINUTE_BLOCKS * 60
DAY_BLOCKS = HOUR_BLOCKS * 24
Ein Block im Ethereum Mainnet entsteht typischerweise alle zwölf Sekunden, daher ist dies die Anzahl der Blöcke, die wir in einem bestimmten Zeitraum erwarten würden. Beachten Sie, dass dies keine exakte Zahl ist. Wenn der Block-Proposer ausfällt, wird dieser Block übersprungen und die Zeit bis zum nächsten Block beträgt 24 Sekunden. Wenn wir den exakten Block für einen Zeitstempel ermitteln wollten, würden wir die binäre Suche (opens in a new tab) verwenden. Für unsere Zwecke ist dies jedoch genau genug. Die Zukunft vorherzusagen ist keine exakte Wissenschaft.
CYCLE_BLOCKS = DAY_BLOCKS
Die Größe des Zyklus. Wir überprüfen die Preisangebote einmal pro Zyklus und versuchen, den Wert am Ende des nächsten Zyklus zu schätzen.
# Die Adresse des Pools, den wir lesen
WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")
Die Preisangebote stammen aus dem Uniswap v3 USDC/WETH-Pool an der Adresse 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 (opens in a new tab). Diese Adresse liegt bereits in Checksum-Form vor, aber es ist besser, Web3.to_checksum_address (opens in a new tab) zu verwenden, um den Code wiederverwendbar zu machen.
POOL_ABI = [
{ "name": "slot0", ... },
{ "name": "token0", ... },
{ "name": "token1", ... },
]
ERC20_ABI = [
{ "name": "symbol", ... },
{ "name": "decimals", ... }
]
Dies sind die ABIs (opens in a new tab) für die beiden Verträge, die wir kontaktieren müssen. Um den Code prägnant zu halten, fügen wir nur die Funktionen ein, die wir aufrufen müssen.
w3 = Web3(Web3.HTTPProvider(MAINNET_URL))
Initialisieren Sie die Web3 (opens in a new tab)-Bibliothek und stellen Sie eine Verbindung zu einem Ethereum-Knoten her.
@dataclass(frozen=True)
class ERC20Token:
address: str
symbol: str
decimals: int
contract: Contract
Dies ist eine Möglichkeit, eine Datenklasse in Python zu erstellen. Der Datentyp Contract (opens in a new tab) wird verwendet, um eine Verbindung zum Vertrag herzustellen. Beachten Sie das (frozen=True). In Python werden Booleans (opens in a new tab) als True oder False definiert, also großgeschrieben. Diese Datenklasse ist frozen, was bedeutet, dass die Felder nicht geändert werden können.
Beachten Sie die Einrückung. Im Gegensatz zu C-abgeleiteten Sprachen (opens in a new tab) verwendet Python Einrückungen, um Blöcke zu kennzeichnen. Der Python-Interpreter weiß, dass die folgende Definition nicht Teil dieser Datenklasse ist, da sie nicht mit derselben Einrückung wie die Felder der Datenklasse beginnt.
@dataclass(frozen=True)
class PoolInfo:
address: str
token0: ERC20Token
token1: ERC20Token
contract: Contract
asset: str
decimal_factor: Decimal = 1
Der Typ Decimal (opens in a new tab) wird für die genaue Handhabung von Dezimalbrüchen verwendet.
def get_price(self, block: int) -> Decimal:
So definiert man eine Funktion in Python. Die Definition ist eingerückt, um zu zeigen, dass sie noch Teil von PoolInfo ist.
In einer Funktion, die Teil einer Datenklasse ist, ist der erste Parameter immer self, die Instanz der Datenklasse, die hier aufgerufen hat. Hier gibt es einen weiteren Parameter, die Blocknummer.
assert block <= w3.eth.block_number, "Block is in the future"
Wenn wir die Zukunft lesen könnten, bräuchten wir keine KI für das Trading.
sqrt_price_x96 = Decimal(self.contract.functions.slot0().call(block_identifier=block)[0])
Die Syntax zum Aufrufen einer Funktion auf der EVM von Web3 aus lautet: <contract object>.functions.<function name>().call(<parameters>). Die Parameter können die Parameter der EVM-Funktion sein (falls vorhanden; hier gibt es keine) oder benannte Parameter (opens in a new tab) zur Änderung des Blockchain-Verhaltens. Hier verwenden wir einen, block_identifier, um die Blocknummer anzugeben, in der wir die Ausführung wünschen.
Das Ergebnis ist dieses Struct in Array-Form (opens in a new tab). Der erste Wert ist eine Funktion des Wechselkurses zwischen den beiden Token.
raw_price = (sqrt_price_x96 / Decimal(2**96)) ** 2
Um Onchain-Berechnungen zu reduzieren, speichert Uniswap v3 nicht den tatsächlichen Umrechnungsfaktor, sondern dessen Quadratwurzel. Da die EVM keine Fließkomma-Mathematik oder Brüche unterstützt, ist die Antwort anstelle des tatsächlichen Wertes
# (Token1 pro Token0)
return 1/(raw_price * self.decimal_factor)
Der Rohpreis, den wir erhalten, ist die Anzahl an token0, die wir für jeden token1 bekommen. In unserem Pool ist token0 USDC (ein Stablecoin mit dem gleichen Wert wie ein US-Dollar) und token1 ist WETH (opens in a new tab). Der Wert, den wir wirklich wollen, ist die Anzahl der Dollar pro WETH, nicht umgekehrt.
Der Dezimalfaktor ist das Verhältnis zwischen den Dezimalfaktoren (opens in a new tab) der beiden Token.
@dataclass(frozen=True)
class Quote:
timestamp: str
price: Decimal
asset: str
Diese Datenklasse repräsentiert ein Preisangebot: den Preis eines bestimmten Assets zu einem bestimmten Zeitpunkt. Zu diesem Zeitpunkt ist das Feld asset irrelevant, da wir einen einzigen Pool verwenden und daher ein einziges Asset haben. Wir werden jedoch später weitere Assets hinzufügen.
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
)
Diese Funktion nimmt eine Adresse entgegen und gibt Informationen über den Token-Vertrag an dieser Adresse zurück. Um einen neuen Web3 Contract (opens in a new tab) zu erstellen, übergeben wir die Adresse und die ABI an 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)
)
Diese Funktion gibt alles zurück, was wir über einen bestimmten Pool (opens in a new tab) benötigen. Die Syntax f"<string>" ist ein formatierter String (opens in a new tab).
def get_quote(pool: PoolInfo, block_number: int = None) -> Quote:
Holen Sie sich ein Quote-Objekt. Der Standardwert für block_number ist None (kein Wert).
if block_number is None:
block_number = w3.eth.block_number
Wenn keine Blocknummer angegeben wurde, verwenden Sie w3.eth.block_number, was die neueste Blocknummer ist. Dies ist die Syntax für eine if-Anweisung (opens in a new tab).
Es mag so aussehen, als wäre es besser gewesen, den Standardwert einfach auf w3.eth.block_number zu setzen, aber das funktioniert nicht gut, da es die Blocknummer zum Zeitpunkt der Funktionsdefinition wäre. In einem lang laufenden Agenten wäre dies ein Problem.
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
)
Verwenden Sie die datetime-Bibliothek (opens in a new tab), um es in ein Format zu formatieren, das für Menschen und große Sprachmodelle (LLMs) lesbar ist. Verwenden Sie Decimal.quantize (opens in a new tab), um den Wert auf zwei Dezimalstellen zu runden.
def get_quotes(pool: PoolInfo, start_block: int, end_block: int, step: int) -> list[Quote]:
In Python definieren Sie eine Liste (opens in a new tab), die nur einen bestimmten Typ enthalten darf, mit list[<type>].
quotes = []
for block in range(start_block, end_block + 1, step):
In Python iteriert eine for-Schleife (opens in a new tab) typischerweise über eine Liste. Die Liste der Blocknummern, in denen nach Preisangeboten gesucht werden soll, stammt von range (opens in a new tab).
quote = get_quote(pool, block)
quotes.append(quote)
return quotes
Holen Sie für jede Blocknummer ein Quote-Objekt und hängen Sie es an die Liste quotes an. Geben Sie dann diese Liste zurück.
pool = read_pool(WETHUSDC_ADDRESS)
quotes = get_quotes(
pool,
w3.eth.block_number - 12*CYCLE_BLOCKS,
w3.eth.block_number,
CYCLE_BLOCKS
)
pprint(quotes)
Dies ist der Hauptcode des Skripts. Lesen Sie die Pool-Informationen, holen Sie zwölf Preisangebote ein und geben Sie sie mit pprint (opens in a new tab) aus.
Erstellen eines Prompts
Als Nächstes müssen wir diese Liste von Preisangeboten in einen Prompt für ein LLM umwandeln und einen erwarteten zukünftigen Wert erhalten.
git checkout 03-create-prompt
uv run agent.py
Die Ausgabe wird nun ein Prompt für ein LLM sein, ähnlich wie:
Angesichts dieser Preisangebote:
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
Welchen Wert würden Sie für WETH/USDC zum Zeitpunkt 2026-02-02T17:56 erwarten?
Geben Sie Ihre Antwort als einzelne Zahl an, gerundet auf zwei Dezimalstellen,
ohne weiteren Text.
Beachten Sie, dass es hier Preisangebote für zwei Assets gibt, WETH/USDC und WBTC/WETH. Das Hinzufügen von Preisangeboten eines weiteren Assets könnte die Vorhersagegenauigkeit verbessern.
Wie ein Prompt aussieht
Dieser Prompt enthält drei Abschnitte, die in LLM-Prompts ziemlich üblich sind.
-
Informationen. LLMs verfügen über viele Informationen aus ihrem Training, aber sie haben normalerweise nicht die neuesten. Aus diesem Grund müssen wir hier die neuesten Preisangebote abrufen. Das Hinzufügen von Informationen zu einem Prompt wird als Retrieval Augmented Generation (RAG) (opens in a new tab) bezeichnet.
-
Die eigentliche Frage. Das ist es, was wir wissen wollen.
-
Anweisungen zur Ausgabeformatierung. Normalerweise gibt uns ein LLM eine Schätzung mit einer Erklärung, wie es dazu gekommen ist. Das ist besser für Menschen, aber ein Computerprogramm benötigt nur das Endergebnis.
Code-Erklärung
Hier ist der neue Code.
from datetime import datetime, timezone, timedelta
Wir müssen dem LLM die Zeit mitteilen, für die wir eine Schätzung wünschen. Um eine Zeit „n Minuten/Stunden/Tage“ in der Zukunft zu erhalten, verwenden wir die Klasse timedelta (opens in a new tab).
# Die Adressen der Pools, die wir lesen
WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")
WETHWBTC_ADDRESS = Web3.to_checksum_address("0xCBCdF9626bC03E24f779434178A73a0B4bad62eD")
Wir haben zwei Pools, die wir lesen müssen.
@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 pro Token0)
if self.reverse:
return 1/(raw_price * self.decimal_factor)
else:
return raw_price * self.decimal_factor
Im WETH/USDC-Pool möchten wir wissen, wie viele token0 (USDC) wir benötigen, um einen token1 (WETH) zu kaufen. Im WETH/WBTC-Pool möchten wir wissen, wie viele token1 (WETH) wir benötigen, um einen token0 (WBTC, also Wrapped Bitcoin) zu kaufen. Wir müssen nachverfolgen, ob das Verhältnis des Pools umgekehrt werden muss.
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
)
Um zu wissen, ob ein Pool umgekehrt werden muss, übergeben wir dies als Eingabe an read_pool. Außerdem muss das Asset-Symbol korrekt eingerichtet werden.
Die Syntax <a> if <b> else <c> ist das Python-Äquivalent des ternären bedingten Operators (opens in a new tab), der in einer C-abgeleiteten Sprache <b> ? <a> : <c> wäre.
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
Diese Funktion erstellt einen String, der eine Liste von Quote-Objekten formatiert, unter der Annahme, dass sie sich alle auf dasselbe Asset beziehen.
def make_prompt(quotes: list[list[Quote]], expected_time: str, asset: str) -> str:
return f"""
In Python werden mehrzeilige String-Literale (opens in a new tab) als """ .... """ geschrieben.
Given these quotes:
{
functools.reduce(lambda acc, q: acc + '\n' + q,
map(lambda q: format_quotes(q), quotes))
}
Hier verwenden wir das MapReduce (opens in a new tab)-Muster, um mit format_quotes einen String für jede Preisangebotsliste zu generieren, und reduzieren sie dann zu einem einzigen String zur Verwendung im Prompt.
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.
"""
Der Rest des Prompts ist wie erwartet.
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
)
Überprüfen Sie die beiden Pools und holen Sie Preisangebote von beiden ein.
future_time = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat()[0:16]
print(make_prompt(wethusdc_quotes + wethwbtc_quotes, future_time, wethusdc_pool.asset))
Bestimmen Sie den zukünftigen Zeitpunkt, für den wir die Schätzung wünschen, und erstellen Sie den Prompt.
Schnittstelle zu einem LLM
Als Nächstes fordern wir ein echtes LLM auf und erhalten einen erwarteten zukünftigen Wert. Ich habe dieses Programm mit OpenAI geschrieben. Wenn Sie also einen anderen Anbieter verwenden möchten, müssen Sie es anpassen.
-
Holen Sie sich ein OpenAI-Konto (opens in a new tab)
-
Laden Sie das Konto auf (opens in a new tab) – der Mindestbetrag zum Zeitpunkt des Schreibens beträgt 5 $
-
Exportieren Sie den API-Schlüssel in der Befehlszeile, damit Ihr Programm ihn verwenden kann
export OPENAI_API_KEY=sk-<the rest of the key goes here> -
Checken Sie den Agenten aus und führen Sie ihn aus
git checkout 04-interface-llm uv run agent.py
Hier ist der neue Code.
from openai import OpenAI
open_ai = OpenAI() # Der Client liest die Umgebungsvariable OPENAI_API_KEY
Importieren und instanziieren Sie die OpenAI-API.
response = open_ai.chat.completions.create(
model="gpt-4-turbo",
messages=[
{"role": "user", "content": prompt}
],
temperature=0.0,
max_tokens=16,
)
Rufen Sie die OpenAI-API (open_ai.chat.completions.create) auf, um die Antwort zu erstellen.
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")
Geben Sie den Preis aus und geben Sie eine Kauf- oder Verkaufsempfehlung ab.
Testen der Vorhersagen
Da wir nun Vorhersagen generieren können, können wir auch historische Daten verwenden, um zu beurteilen, ob wir nützliche Vorhersagen produzieren.
uv run test-predictor.py
Das erwartete Ergebnis sieht ähnlich aus wie:
Vorhersage für 2026-01-05T19:50: vorhergesagt 3138.93 USD, real 3218.92 USD, Fehler 79.99 USD
Vorhersage für 2026-01-06T19:56: vorhergesagt 3243.39 USD, real 3221.08 USD, Fehler 22.31 USD
Vorhersage für 2026-01-07T20:02: vorhergesagt 3223.24 USD, real 3146.89 USD, Fehler 76.35 USD
Vorhersage für 2026-01-08T20:11: vorhergesagt 3150.47 USD, real 3092.04 USD, Fehler 58.43 USD
.
.
.
Vorhersage für 2026-01-31T22:33: vorhergesagt 2637.73 USD, real 2417.77 USD, Fehler 219.96 USD
Vorhersage für 2026-02-01T22:41: vorhergesagt 2381.70 USD, real 2318.84 USD, Fehler 62.86 USD
Vorhersage für 2026-02-02T22:49: vorhergesagt 2234.91 USD, real 2349.28 USD, Fehler 114.37 USD
Mittlerer Vorhersagefehler über 29 Vorhersagen: 83.87103448275862068965517241 USD
Mittlere Änderung pro Empfehlung: 4.787931034482758620689655172 USD
Standardvarianz der Änderungen: 104.42 USD
Profitable Tage: 51.72%
Verlusttage: 48.28%
Der Großteil des Testers ist identisch mit dem Agenten, aber hier sind die Teile, die neu oder geändert sind.
CYCLES_FOR_TEST = 40 # Für den Backtest, über wie viele Zyklen wir testen
# Viele Kurse abrufen
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
)
Wir blicken CYCLES_FOR_TEST (hier als 40 angegeben) Tage zurück.
# Vorhersagen erstellen und mit der echten Historie abgleichen
total_error = Decimal(0)
changes = []
Es gibt zwei Arten von Fehlern, an denen wir interessiert sind. Der erste, total_error, ist einfach die Summe der Fehler, die der Prädiktor gemacht hat.
Um den zweiten, changes, zu verstehen, müssen wir uns an den Zweck des Agenten erinnern. Er besteht nicht darin, das WETH/USDC-Verhältnis (ETH-Preis) vorherzusagen. Er besteht darin, Verkaufs- und Kaufempfehlungen abzugeben. Wenn der Preis derzeit 2000 $ beträgt und er für morgen 2010 $ vorhersagt, stört es uns nicht, wenn das tatsächliche Ergebnis 2020 $ ist und wir zusätzliches Geld verdienen. Aber es stört uns sehr wohl, wenn er 2010 $ vorhergesagt hat, wir basierend auf dieser Empfehlung ETH gekauft haben und der Preis auf 1990 $ fällt.
for index in range(0,len(wethusdc_quotes)-CYCLES_BACK):
Wir können uns nur Fälle ansehen, in denen die vollständige Historie (die für die Vorhersage verwendeten Werte und der reale Wert zum Vergleich) verfügbar ist. Das bedeutet, dass der neueste Fall derjenige sein muss, der vor CYCLES_BACK begonnen hat.
wethusdc_slice = wethusdc_quotes[index:index+CYCLES_BACK]
wethwbtc_slice = wethwbtc_quotes[index:index+CYCLES_BACK]
Verwenden Sie Slices (opens in a new tab), um die gleiche Anzahl an Stichproben zu erhalten wie die Anzahl, die der Agent verwendet. Der Code zwischen hier und dem nächsten Segment ist derselbe Code zur Vorhersagegewinnung, den wir im Agenten haben.
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
Holen Sie den vorhergesagten Preis, den realen Preis und den Preis zum Zeitpunkt der Vorhersage. Wir benötigen den Preis zum Zeitpunkt der Vorhersage, um zu bestimmen, ob die Empfehlung Kaufen oder Verkaufen lautete.
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")
Berechnen Sie den Fehler und addieren Sie ihn zur Gesamtsumme.
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)
Für changes wollen wir die monetären Auswirkungen des Kaufs oder Verkaufs von einem ETH. Zuerst müssen wir also die Empfehlung bestimmen, dann beurteilen, wie sich der tatsächliche Preis verändert hat, und ob die Empfehlung Geld eingebracht (positive Veränderung) oder Geld gekostet hat (negative Veränderung).
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")
Berichten Sie die Ergebnisse.
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%}")
Verwenden Sie filter (opens in a new tab), um die Anzahl der profitablen Tage und die Anzahl der verlustreichen Tage zu zählen. Das Ergebnis ist ein Filterobjekt, das wir in eine Liste umwandeln müssen, um die Länge zu erhalten.
Einreichen von Transaktionen
Jetzt müssen wir tatsächlich Transaktionen einreichen. Ich möchte jedoch zu diesem Zeitpunkt kein echtes Geld ausgeben, bevor sich das System bewährt hat. Stattdessen werden wir einen lokalen Fork des Mainnets erstellen und in diesem Netzwerk „handeln“.
Hier sind die Schritte, um einen lokalen Fork zu erstellen und den Handel zu ermöglichen.
-
Installieren Sie Foundry (opens in a new tab)
-
Starten Sie
anvil(opens in a new tab)anvil --fork-url https://eth.drpc.org --block-time 12anvillauscht auf der Standard-URL für Foundry, http://localhost:8545 (opens in a new tab), sodass wir die URL für den Befehlcast(opens in a new tab), den wir zur Manipulation der Blockchain verwenden, nicht angeben müssen. -
Bei der Ausführung in
anvilgibt es zehn Testkonten, die über ETH verfügen – legen Sie die Umgebungsvariablen für das erste festPRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 ADDRESS=`cast wallet address $PRIVATE_KEY` -
Dies sind die Verträge, die wir verwenden müssen.
SwapRouter(opens in a new tab) ist der Uniswap v3-Vertrag, den wir für den eigentlichen Handel verwenden. Wir könnten direkt über den Pool handeln, aber dies ist viel einfacher.Die beiden unteren Variablen sind die Uniswap v3-Pfade, die für den Tausch zwischen WETH und USDC erforderlich sind.
WETH_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C05861564 WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 -
Jedes der Testkonten verfügt über 10.000 ETH. Verwenden Sie den WETH-Vertrag, um 1000 ETH zu wrappen, um 1000 WETH für den Handel zu erhalten.
cast send $WETH_ADDRESS "deposit()" --value 1000ether --private-key $PRIVATE_KEY -
Verwenden Sie
SwapRouter, um 500 WETH gegen USDC zu tauschen.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_KEYDer Aufruf von
approveerstellt einen Freigabebetrag, der esSwapRoutererlaubt, einige unserer Token auszugeben. Verträge können keine Ereignisse überwachen. Wenn wir also Token direkt an den VertragSwapRouterübertragen, wüsste dieser nicht, dass er bezahlt wurde. Stattdessen genehmigen wir dem VertragSwapRouter, einen bestimmten Betrag auszugeben, und dann tutSwapRouterdies. Dies geschieht über eine vonSwapRouteraufgerufene Funktion, sodass er weiß, ob es erfolgreich war. -
Überprüfen Sie, ob Sie von beiden Token genug haben.
cast call $WETH_ADDRESS "balanceOf(address)" $ADDRESS | cast from-wei echo `cast call $USDC_ADDRESS "balanceOf(address)" $ADDRESS | cast to-dec`/10^6 | bc
Da wir nun WETH und USDC haben, können wir den Agenten tatsächlich ausführen.
git checkout 05-trade
uv run agent.py
Die Ausgabe wird ähnlich aussehen wie:
(ai-trading-agent) qbzzt@Ori-Cloudnomics:~/260215-ai-agent$ uv run agent.py
Aktueller Preis: 1843.16
Am 2026-02-06T23:07, erwarteter Preis: 1724.41 USD
Kontostände vor dem Trade:
USDC-Guthaben: 927301.578272
WETH-Guthaben: 500
Verkaufen, ich erwarte, dass der Preis um 118.75 USD sinkt
Genehmigungs-Transaktion gesendet: 74e367ddbb407c1aaf567d87aa5863049991b1d2aa092b6b85195d925e2bd41f
Genehmigungs-Transaktion gemint.
Verkaufs-Transaktion gesendet: fad1bcf938585c9e90364b26ac7a80eea9efd34c37e5db81e58d7655bcae28bf
Verkaufs-Transaktion gemint.
Kontostände nach dem Trade:
USDC-Guthaben: 929143.797116
WETH-Guthaben: 499
Um ihn tatsächlich zu nutzen, benötigen Sie ein paar kleine Änderungen.
- Ändern Sie in Zeile 14
MAINNET_URLin einen echten Zugangspunkt, wie z. B.https://eth.drpc.org - Ändern Sie in Zeile 28
PRIVATE_KEYin Ihren eigenen privaten Schlüssel - Sofern Sie nicht sehr wohlhabend sind und jeden Tag 1 ETH für einen unbewiesenen Agenten kaufen oder verkaufen können, möchten Sie vielleicht Zeile 29 ändern, um
WETH_TRADE_AMOUNTzu verringern
Code-Erklärung
Hier ist der neue Code.
SWAP_ROUTER_ADDRESS=Web3.to_checksum_address("0xE592427A0AEce92De3Edee1F18E0157C05861564")
WETH_TO_USDC=bytes.fromhex("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
USDC_TO_WETH=bytes.fromhex("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
Dieselben Variablen, die wir in Schritt 4 verwendet haben.
WETH_TRADE_AMOUNT=1
Der zu handelnde Betrag.
ERC20_ABI = [
{ "name": "symbol", ... },
{ "name": "decimals", ... },
{ "name": "balanceOf", ...},
{ "name": "approve", ...}
]
Um tatsächlich zu handeln, benötigen wir die Funktion approve. Wir möchten auch die Guthaben davor und danach anzeigen, also benötigen wir auch balanceOf.
SWAP_ROUTER_ABI = [
{ "name": "exactInput", ...},
]
In der SwapRouter-ABI benötigen wir nur exactInput. Es gibt eine verwandte Funktion, exactOutput, die wir verwenden könnten, um genau einen WETH zu kaufen, aber der Einfachheit halber verwenden wir in beiden Fällen einfach exactInput.
account = w3.eth.account.from_key(PRIVATE_KEY)
swap_router = w3.eth.contract(
address=SWAP_ROUTER_ADDRESS,
abi=SWAP_ROUTER_ABI
)
Die Web3-Definitionen für den account (opens in a new tab) und den Vertrag SwapRouter.
def txn_params() -> dict:
return {
"from": account.address,
"value": 0,
"gas": 300000,
"nonce": w3.eth.get_transaction_count(account.address),
}
Die Transaktionsparameter. Wir benötigen hier eine Funktion, da sich die Nonce (opens in a new tab) jedes Mal ändern muss.
def approve_token(contract: Contract, amount: int):
Genehmigen Sie einen Freigabebetrag für 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)
So senden wir eine Transaktion in Web3. Zuerst verwenden wir das Contract-Objekt (opens in a new tab), um die Transaktion zu erstellen. Dann verwenden wir web3.eth.account.sign_transaction (opens in a new tab), um die Transaktion mit PRIVATE_KEY zu signieren. Schließlich verwenden wir w3.eth.send_raw_transaction (opens in a new tab), um die Transaktion zu senden.
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) wartet, bis die Transaktion gemint ist. Es gibt bei Bedarf den Transaktionsbeleg zurück.
SELL_PARAMS = {
"path": WETH_TO_USDC,
"recipient": account.address,
"deadline": 2**256 - 1,
"amountIn": WETH_TRADE_AMOUNT * 10 ** wethusdc_pool.token1.decimals,
"amountOutMinimum": 0,
}
Dies sind die Parameter beim Verkauf von 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,
}
Im Gegensatz zu SELL_PARAMS können sich die Kaufparameter ändern. Der Eingabebetrag ist der Preis für 1 WETH, wie in quote verfügbar.
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.")
Die Funktionen buy() und sell() sind nahezu identisch. Zuerst genehmigen wir einen ausreichenden Freigabebetrag für SwapRouter, und dann rufen wir ihn mit dem korrekten Pfad und Betrag auf.
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)}")
Berichten Sie die Benutzerguthaben in beiden Währungen.
print("Account balances before trade:")
balances()
if (expected_price > current_price):
print(f"Buy, I expect the price to go up by {expected_price - current_price} USD")
buy(wethusdc_quotes[-1])
else:
print(f"Sell, I expect the price to go down by {current_price - expected_price} USD")
sell()
print("Account balances after trade:")
balances()
Dieser Agent funktioniert derzeit nur einmal. Sie können ihn jedoch so ändern, dass er kontinuierlich arbeitet, indem Sie ihn entweder über crontab (opens in a new tab) ausführen oder die Zeilen 368-400 in eine Schleife packen und time.sleep (opens in a new tab) verwenden, um zu warten, bis es Zeit für den nächsten Zyklus ist.
Mögliche Verbesserungen
Dies ist keine vollständige Produktionsversion; es ist lediglich ein Beispiel, um die Grundlagen zu vermitteln. Hier sind einige Ideen für Verbesserungen.
Intelligenteres Trading
Es gibt zwei wichtige Fakten, die der Agent bei der Entscheidung, was zu tun ist, ignoriert.
- Das Ausmaß der erwarteten Änderung. Der Agent verkauft einen festen Betrag an
WETH, wenn ein Preisrückgang erwartet wird, unabhängig vom Ausmaß des Rückgangs. Es wäre wohl besser, geringfügige Änderungen zu ignorieren und basierend darauf zu verkaufen, wie stark der Preis voraussichtlich fallen wird. - Das aktuelle Portfolio. Wenn 10 % Ihres Portfolios in WETH sind und Sie glauben, dass der Preis steigen wird, macht es wahrscheinlich Sinn, mehr zu kaufen. Wenn jedoch 90 % Ihres Portfolios in WETH sind, sind Sie möglicherweise ausreichend exponiert und es besteht keine Notwendigkeit, mehr zu kaufen. Das Gegenteil gilt, wenn Sie erwarten, dass der Preis sinkt.
Was ist, wenn Sie Ihre Trading-Strategie geheim halten wollen?
KI-Anbieter können die Abfragen sehen, die Sie an ihre LLMs senden, was das geniale Trading-System offenlegen könnte, das Sie mit Ihrem Agenten entwickelt haben. Ein Trading-System, das von zu vielen Menschen genutzt wird, ist wertlos, da zu viele Menschen versuchen zu kaufen, wenn Sie kaufen wollen (und der Preis steigt), und versuchen zu verkaufen, wenn Sie verkaufen wollen (und der Preis sinkt).
Sie können ein LLM lokal ausführen, zum Beispiel mit LM-Studio (opens in a new tab), um dieses Problem zu vermeiden.
Vom KI-Bot zum KI-Agenten
Man kann gut argumentieren, dass dies ein KI-Bot und kein KI-Agent ist. Er implementiert eine relativ einfache Strategie, die auf vordefinierten Informationen beruht. Wir können Selbstverbesserung ermöglichen, indem wir beispielsweise eine Liste von Uniswap v3-Pools und deren neuesten Werten bereitstellen und fragen, welche Kombination den besten Vorhersagewert hat.
Slippage-Schutz
Derzeit gibt es keinen Slippage-Schutz (opens in a new tab). Wenn das aktuelle Preisangebot 2000 $ beträgt und der erwartete Preis 2100 $ ist, wird der Agent kaufen. Wenn die Kosten jedoch vor dem Kauf durch den Agenten auf 2200 $ steigen, macht es keinen Sinn mehr zu kaufen.
Um einen Slippage-Schutz zu implementieren, geben Sie einen amountOutMinimum-Wert in den Zeilen 325 und 334 von agent.py (opens in a new tab) an.
Fazit
Hoffentlich wissen Sie jetzt genug, um mit KI-Agenten loszulegen. Dies ist kein umfassender Überblick über das Thema; es gibt ganze Bücher, die sich dem widmen, aber dies reicht aus, um Ihnen den Einstieg zu erleichtern. Viel Erfolg!