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, zusammen mit 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 verwendet. All dies sind Schritte, die für einen KI-Agenten erforderlich sind. Dieser Agent ist in Python implementiert, da dies die am häufigsten verwendete Sprache in der KI ist.
Warum das tun?
Automatisierte Trading-Agenten ermöglichen es Entwicklern, eine Handelsstrategie auszuwählen und auszuführen. KI-Agenten ermöglichen komplexere und dynamischere Handelsstrategien, die potenziell Informationen und Algorithmen nutzen, an deren Verwendung der Entwickler noch nicht einmal 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 klar, 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 verteilte Börsen (DEX), die es Ihnen ermöglichen, Token auf Ethereum zu handeln. Sie neigen jedoch dazu, aufgrund von Arbitrage ähnliche Wechselkurse zu haben.
Uniswap (opens in a new tab) ist eine weit verbreitete DEX, die wir sowohl für Preisangebote (um die relativen Werte der Token zu sehen) als auch für den Handel nutzen können.
OpenAI
Für ein großes Sprachmodell habe ich mich für den Einstieg für 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
Es gibt 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.
1git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started2cd 260215-ai-agent
1
23. Installieren Sie [`uv`](https://docs.astral.sh/uv/getting-started/installation/). Der Befehl auf Ihrem System könnte anders lauten.3
4 ```sh5 pipx install uv-
Laden Sie die Bibliotheken herunter.
1uv sync
1
25. Aktivieren Sie die virtuelle Umgebung.3
4 ```sh5 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.1from web3 import Web32MAINNET_URL = "https://eth.drpc.org"3w3 = Web3(Web3.HTTPProvider(MAINNET_URL))4w3.eth.block_number5quit()
1
2### Lesen aus der Blockchain \{#read-blockchain\}3
4Der 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.5
6```sh7git checkout 02-read-quote8uv run agent.pySie 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.
1from web3 import Web32from web3.contract import Contract3from decimal import Decimal, ROUND_HALF_UP4from dataclasses import dataclass5from datetime import datetime, timezone6from pprint import pprint7import time8import functools9import sysImportieren Sie die benötigten Bibliotheken. Sie werden unten erklärt, wenn sie verwendet werden.
1print = functools.partial(print, flush=True)Ersetzt Pythons print durch eine Version, die die Ausgabe immer sofort leert. Dies ist in einem lang laufenden Skript nützlich, da wir nicht auf Statusaktualisierungen oder Debugging-Ausgaben warten möchten.
1MAINNET_URL = "https://eth.drpc.org"Eine URL, um zum Mainnet zu gelangen. Sie können eine von Blockchain-Knoten als Dienstleistung erhalten oder eine der auf Chainlist (opens in a new tab) beworbenen verwenden.
1BLOCK_TIME_SECONDS = 122MINUTE_BLOCKS = int(60 / BLOCK_TIME_SECONDS)3HOUR_BLOCKS = MINUTE_BLOCKS * 604DAY_BLOCKS = HOUR_BLOCKS * 24Ein Ethereum-Mainnet-Block 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-Vorschlagende ausfällt, wird dieser Block übersprungen, und die Zeit für den nächsten Block beträgt 24 Sekunden. Wenn wir den exakten Block für einen Zeitstempel erhalten wollten, würden wir die binäre Suche (opens in a new tab) verwenden. Für unsere Zwecke ist dies jedoch nah genug. Die Zukunft vorherzusagen ist keine exakte Wissenschaft.
1CYCLE_BLOCKS = DAY_BLOCKSDie 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.
1# Die Adresse des Pools, den wir auslesen2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")Die Werte der Preisangebote stammen aus dem Uniswap 3 USDC/WETH-Pool an der Adresse 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 (opens in a new tab). Diese Adresse liegt bereits in Prüfsummenform vor, aber es ist besser, Web3.to_checksum_address (opens in a new tab) zu verwenden, um den Code wiederverwendbar zu machen.
1POOL_ABI = [2 { "name": "slot0", ... },3 { "name": "token0", ... },4 { "name": "token1", ... },5]6
7ERC20_ABI = [8 { "name": "symbol", ... },9 { "name": "decimals", ... }10]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.
1w3 = Web3(Web3.HTTPProvider(MAINNET_URL))Initialisieren Sie die Web3 (opens in a new tab)-Bibliothek und stellen Sie eine Verbindung zu einem Ethereum-Blockchain-Knoten her.
1@dataclass(frozen=True)2class ERC20Token:3 address: str4 symbol: str5 decimals: int6 contract: ContractDies 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, großgeschrieben. Diese Datenklasse ist frozen (eingefroren), 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.
1@dataclass(frozen=True)2class PoolInfo:3 address: str4 token0: ERC20Token5 token1: ERC20Token6 contract: Contract7 asset: str8 decimal_factor: Decimal = 1Der Typ Decimal (opens in a new tab) wird für die genaue Handhabung von Dezimalbrüchen verwendet.
1 def get_price(self, block: int) -> Decimal:Dies ist die Art und Weise, eine Funktion in Python zu definieren. Die Definition ist eingerückt, um zu zeigen, dass sie immer 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.
1 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.
1 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 ausführen möchten.
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.
1 raw_price = (sqrt_price_x96 / Decimal(2**96)) ** 2Um Berechnungen auf der Blockchain 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
1 # (token1 pro token0)2 return 1/(raw_price * self.decimal_factor)Der Rohpreis, den wir erhalten, ist die Anzahl von token0, die wir für jedes token1 bekommen. In unserem Pool ist token0 USDC (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) für die beiden Token.
1@dataclass(frozen=True)2class Quote:3 timestamp: str4 price: Decimal5 asset: strDiese Datenklasse repräsentiert ein Preisangebot (Quote): den Preis eines bestimmten Assets zu einem bestimmten Zeitpunkt. An diesem Punkt 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.
1def read_token(address: str) -> ERC20Token:2 token = w3.eth.contract(address=address, abi=ERC20_ABI)3 symbol = token.functions.symbol().call()4 decimals = token.functions.decimals().call()5
6 return ERC20Token(7 address=address,8 symbol=symbol,9 decimals=decimals,10 contract=token11 )Diese Funktion nimmt eine Adresse 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.
1def read_pool(address: str) -> PoolInfo:2 pool_contract = w3.eth.contract(address=address, abi=POOL_ABI)3 token0Address = pool_contract.functions.token0().call()4 token1Address = pool_contract.functions.token1().call()5 token0 = read_token(token0Address)6 token1 = read_token(token1Address)7
8 return PoolInfo(9 address=address,10 asset=f"{token1.symbol}/{token0.symbol}",11 token0=token0,12 token1=token1,13 contract=pool_contract,14 decimal_factor=Decimal(10) ** Decimal(token0.decimals - token1.decimals)15 )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).
1def 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).
1 if block_number is None:2 block_number = w3.eth.block_numberWenn 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 Definition der Funktion wäre. In einem lang laufenden Agenten wäre dies ein Problem.
1 block = w3.eth.get_block(block_number)2 price = pool.get_price(block_number)3 return Quote(4 timestamp=datetime.fromtimestamp(block.timestamp, timezone.utc).isoformat(),5 price=price.quantize(Decimal("0.01")),6 asset=pool.asset7 )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.
1def 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 kann, mit list[<type>].
1 quotes = []2 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).
1 quote = get_quote(pool, block)2 quotes.append(quote)3 return quotesHolen 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.
1pool = read_pool(WETHUSDC_ADDRESS)2quotes = get_quotes(3 pool,4 w3.eth.block_number - 12*CYCLE_BLOCKS,5 w3.eth.block_number,6 CYCLE_BLOCKS7)8
9pprint(quotes)Dies ist der Hauptcode des Skripts. Lesen Sie die Pool-Informationen, holen Sie zwölf Preisangebote 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.
1git checkout 03-create-prompt2uv run agent.pyDie Ausgabe wird nun ein Prompt an ein LLM sein, ähnlich wie:
1Given these quotes:2Asset: WETH/USDC3 2026-01-20T16:34 3016.214 .5 .6 .7 2026-02-01T17:49 2299.108
9Asset: WBTC/WETH10 2026-01-20T16:34 29.8411 .12 .13 .14 2026-02-01T17:50 33.4615
16
17What would you expect the value for WETH/USDC to be at time 2026-02-02T17:56?18
19Provide your answer as a single number rounded to two decimal places,20without any other text.Beachten Sie, dass es hier Preisangebote für zwei Assets gibt, WETH/USDC und WBTC/WETH. Das Hinzufügen von Preisangeboten eines anderen 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 haben viele Informationen aus ihrem Training, aber sie verfügen normalerweise nicht über 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.
1from datetime import datetime, timezone, timedeltaWir 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).
1# Die Adressen der Pools, die wir auslesen2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")3WETHWBTC_ADDRESS = Web3.to_checksum_address("0xCBCdF9626bC03E24f779434178A73a0B4bad62eD")Wir haben zwei Pools, die wir lesen müssen.
1@dataclass(frozen=True)2class PoolInfo:3 .4 .5 .6 reverse: bool = False7
8 def get_price(self, block: int) -> Decimal:9 assert block <= w3.eth.block_number, "Block is in the future"10 sqrt_price_x96 = Decimal(self.contract.functions.slot0().call(block_identifier=block)[0])11 raw_price = (sqrt_price_x96 / Decimal(2**96)) ** 2 # (token1 pro token0)12 if self.reverse:13 return 1/(raw_price * self.decimal_factor)14 else:15 return raw_price * self.decimal_factorIm WETH/USDC-Pool möchten wir wissen, wie viele von token0 (USDC) wir benötigen, um eines von token1 (WETH) zu kaufen. Im WETH/WBTC-Pool möchten wir wissen, wie viele token1 (WETH) wir benötigen, um ein token0 (WBTC, was Wrapped Bitcoin ist) zu kaufen. Wir müssen nachverfolgen, ob das Verhältnis des Pools umgekehrt werden muss.
1def read_pool(address: str, reverse: bool = False) -> PoolInfo:2 .3 .4 .5
6 return PoolInfo(7 .8 .9 .10
11 asset= f"{token1.symbol}/{token0.symbol}" if reverse else f"{token0.symbol}/{token1.symbol}",12 reverse=reverse13 )Um zu wissen, ob ein Pool umgekehrt werden muss, müssen wir dies als Eingabe für read_pool erhalten. Außerdem muss das Asset-Symbol korrekt eingerichtet sein.
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.
1def format_quotes(quotes: list[Quote]) -> str:2 result = f"Asset: {quotes[0].asset}\n"3 for quote in quotes:4 result += f"\t{quote.timestamp[0:16]} {quote.price.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)}\n"5 return resultDiese Funktion erstellt einen String, der eine Liste von Quote-Objekten formatiert, unter der Annahme, dass sie alle für dasselbe Asset gelten.
1def make_prompt(quotes: list[list[Quote]], expected_time: str, asset: str) -> str:2 return f"""In Python werden mehrzeilige String-Literale (opens in a new tab) als """ .... """ geschrieben.
1Given these quotes:2{3 functools.reduce(lambda acc, q: acc + '\n' + q,4 map(lambda q: format_quotes(q), quotes))5}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.
1What would you expect the value for {asset} to be at time {expected_time}?2
3Provide your answer as a single number rounded to two decimal places,4without any other text.5 """Der Rest des Prompts ist wie erwartet.
1wethusdc_pool = read_pool(WETHUSDC_ADDRESS, True)2wethusdc_quotes = get_quotes(3 wethusdc_pool,4 w3.eth.block_number - 12*CYCLE_BLOCKS,5 w3.eth.block_number,6 CYCLE_BLOCKS,7)8
9wethwbtc_pool = read_pool(WETHWBTC_ADDRESS)10wethwbtc_quotes = get_quotes(11 wethwbtc_pool,12 w3.eth.block_number - 12*CYCLE_BLOCKS,13 w3.eth.block_number,14 CYCLE_BLOCKS15)Überprüfen Sie die beiden Pools und holen Sie Preisangebote von beiden ein.
1future_time = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat()[0:16]2
3print(make_prompt(wethusdc_quotes + wethwbtc_quotes, future_time, wethusdc_pool.asset))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 tatsächliches 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 in der Befehlszeile den API-Schlüssel, damit Ihr Programm ihn verwenden kann
1export OPENAI_API_KEY=sk-<the rest of the key goes here>
1
25. Checken Sie den Agenten aus und führen Sie ihn aus3
4 ```sh5 git checkout 04-interface-llm6 uv run agent.pyHier ist der neue Code.
1from openai import OpenAI2
3open_ai = OpenAI() # Der Client liest die Umgebungsvariable OPENAI_API_KEYImportieren und instanziieren Sie die OpenAI-API.
1response = open_ai.chat.completions.create(2 model="gpt-4-turbo",3 messages=[4 {"role": "user", "content": prompt}5 ],6 temperature=0.0,7 max_tokens=16,8)Rufen Sie die OpenAI-API auf (open_ai.chat.completions.create), um die Antwort zu erstellen.
1expected_price = Decimal(response.choices[0].message.content.strip())2current_price = wethusdc_quotes[-1].price3
4print ("Current price:", wethusdc_quotes[-1].price)5print(f"In {future_time}, expected price: {expected_price} USD")6
7if (expected_price > current_price):8 print(f"Buy, I expect the price to go up by {expected_price - current_price} USD")9else:10 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.
1uv run test-predictor.pyDas erwartete Ergebnis ist ähnlich wie:
1Prediction for 2026-01-05T19:50: predicted 3138.93 USD, real 3218.92 USD, error 79.99 USD2Prediction for 2026-01-06T19:56: predicted 3243.39 USD, real 3221.08 USD, error 22.31 USD3Prediction for 2026-01-07T20:02: predicted 3223.24 USD, real 3146.89 USD, error 76.35 USD4Prediction for 2026-01-08T20:11: predicted 3150.47 USD, real 3092.04 USD, error 58.43 USD5.6.7.8Prediction for 2026-01-31T22:33: predicted 2637.73 USD, real 2417.77 USD, error 219.96 USD9Prediction for 2026-02-01T22:41: predicted 2381.70 USD, real 2318.84 USD, error 62.86 USD10Prediction for 2026-02-02T22:49: predicted 2234.91 USD, real 2349.28 USD, error 114.37 USD11Mean prediction error over 29 predictions: 83.87103448275862068965517241 USD12Mean change per recommendation: 4.787931034482758620689655172 USD13Standard variance of changes: 104.42 USD14Profitable days: 51.72%15Losing days: 48.28%Der Großteil des Testers ist identisch mit dem Agenten, aber hier sind die Teile, die neu oder modifiziert sind.
1CYCLES_FOR_TEST = 40 # Für den Backtest, über wie viele Zyklen wir testen2
3# Viele Kurse abrufen4wethusdc_pool = read_pool(WETHUSDC_ADDRESS, True)5wethusdc_quotes = get_quotes(6 wethusdc_pool,7 w3.eth.block_number - CYCLE_BLOCKS*CYCLES_FOR_TEST,8 w3.eth.block_number,9 CYCLE_BLOCKS,10)11
12wethwbtc_pool = read_pool(WETHWBTC_ADDRESS)13wethwbtc_quotes = get_quotes(14 wethwbtc_pool,15 w3.eth.block_number - CYCLE_BLOCKS*CYCLES_FOR_TEST,16 w3.eth.block_number,17 CYCLE_BLOCKS18)Wir schauen CYCLES_FOR_TEST (hier als 40 angegeben) Tage zurück.
1# Vorhersagen erstellen und mit der echten Historie abgleichen2
3total_error = Decimal(0)4changes = []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. Es geht nicht darum, das WETH/USDC-Verhältnis (ETH-Preis) vorherzusagen. Es geht darum, Verkaufs- und Kaufempfehlungen abzugeben. Wenn der Preis derzeit 2000 $ beträgt und er für morgen 2010 $ vorhersagt, macht es uns nichts aus, 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 und basierend auf dieser Empfehlung ETH gekauft hat, und der Preis auf 1990 $ fällt.
1for 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.
1 wethusdc_slice = wethusdc_quotes[index:index+CYCLES_BACK]2 wethwbtc_slice = wethwbtc_quotes[index:index+CYCLES_BACK]Verwenden Sie Slices (opens in a new tab), um die gleiche Anzahl von Stichproben zu erhalten wie die Anzahl, die der Agent verwendet. Der Code zwischen hier und dem nächsten Segment ist derselbe Code zum Abrufen einer Vorhersage, den wir im Agenten haben.
1 predicted_price = Decimal(response.choices[0].message.content.strip())2 real_price = wethusdc_quotes[index+CYCLES_BACK].price3 prediction_time_price = wethusdc_quotes[index+CYCLES_BACK-1].priceHolen Sie sich 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.
1 error = abs(predicted_price - real_price)2 total_error += error3 print (f"Prediction for {prediction_time}: predicted {predicted_price} USD, real {real_price} USD, error {error} USD")Berechnen Sie den Fehler und fügen Sie ihn der Gesamtsumme hinzu.
1 recomended_action = 'buy' if predicted_price > prediction_time_price else 'sell'2 price_increase = real_price - prediction_time_price3 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).
1print (f"Mean prediction error over {len(wethusdc_quotes)-CYCLES_BACK} predictions: {total_error / Decimal(len(wethusdc_quotes)-CYCLES_BACK)} USD")2
3length_changes = Decimal(len(changes))4mean_change = sum(changes, Decimal(0)) / length_changes5print (f"Mean change per recommendation: {mean_change} USD")6var = sum((x - mean_change) ** 2 for x in changes) / length_changes7print (f"Standard variance of changes: {var.sqrt().quantize(Decimal("0.01"))} USD")Berichten Sie die Ergebnisse.
1print (f"Profitable days: {len(list(filter(lambda x: x > 0, changes)))/length_changes:.2%}")2print (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 das System nicht erprobt ist. 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)1anvil --fork-url https://eth.drpc.org --block-time 12
1
2 `anvil` lauscht auf der Standard-URL für Foundry, http://localhost:8545, sodass wir die URL für [den `cast`-Befehl](https://getfoundry.sh/cast/overview), den wir zur Manipulation der Blockchain verwenden, nicht angeben müssen.3
43. Wenn Sie in `anvil` ausführen, gibt es zehn Testkonten, die über ETH verfügen – legen Sie die Umgebungsvariablen für das erste fest.5
6 ```sh7 PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff808 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 erforderlich sind, um zwischen WETH und USDC zu tauschen.
1WETH_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc22USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f56404SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C058615645WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB486USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
1
25. 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.3
4 ```sh5 cast send $WETH_ADDRESS "deposit()" --value 1000ether --private-key $PRIVATE_KEY-
Verwenden Sie
SwapRouter, um 500 WETH gegen USDC zu handeln.1cast send $WETH_ADDRESS "approve(address,uint256)" $SWAP_ROUTER 500ether --private-key $PRIVATE_KEY2MAXINT=`cast max-int uint256`3cast send $SWAP_ROUTER \4 "exactInput((bytes,address,uint256,uint256,uint256))" \5 "($WETH_TO_USDC,$ADDRESS,$MAXINT,500ether,1000000)" \6 --private-key $PRIVATE_KEY
1
2 Der `approve`-Aufruf erstellt eine Freigabe (Allowance), die es `SwapRouter` ermöglicht, einige unserer Token auszugeben. Verträge können keine Ereignisse überwachen. Wenn wir also Token direkt an den `SwapRouter`-Vertrag übertragen, wüsste dieser nicht, dass er bezahlt wurde. Stattdessen erlauben wir dem `SwapRouter`-Vertrag, einen bestimmten Betrag auszugeben, und dann tut `SwapRouter` dies. Dies geschieht über eine von `SwapRouter` aufgerufene Funktion, sodass er weiß, ob er erfolgreich war.3
47. Überprüfen Sie, ob Sie genug von beiden Token haben.5
6 ```sh7 cast call $WETH_ADDRESS "balanceOf(address)" $ADDRESS | cast from-wei8 echo `cast call $USDC_ADDRESS "balanceOf(address)" $ADDRESS | cast to-dec`/10^6 | bcDa wir nun WETH und USDC haben, können wir den Agenten tatsächlich ausführen.
1git checkout 05-trade2uv run agent.pyDie Ausgabe wird ähnlich aussehen wie:
1(ai-trading-agent) qbzzt@Ori-Cloudnomics:~/260215-ai-agent$ uv run agent.py2Current price: 1843.163In 2026-02-06T23:07, expected price: 1724.41 USD4Account balances before trade:5USDC Balance: 927301.5782726WETH Balance: 5007Sell, I expect the price to go down by 118.75 USD8Approve transaction sent: 74e367ddbb407c1aaf567d87aa5863049991b1d2aa092b6b85195d925e2bd41f9Approve transaction mined.10Sell transaction sent: fad1bcf938585c9e90364b26ac7a80eea9efd34c37e5db81e58d7655bcae28bf11Sell transaction mined.12Account balances after trade:13USDC Balance: 929143.79711614WETH Balance: 499Um 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 Private-Key - Es sei denn, Sie sind sehr wohlhabend und können jeden Tag 1 ETH für einen unbewiesenen Agenten kaufen oder verkaufen, möchten Sie vielleicht Zeile 29 ändern, um
WETH_TRADE_AMOUNTzu verringern
Code-Erklärung
Hier ist der neue Code.
1SWAP_ROUTER_ADDRESS=Web3.to_checksum_address("0xE592427A0AEce92De3Edee1F18E0157C05861564")2WETH_TO_USDC=bytes.fromhex("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")3USDC_TO_WETH=bytes.fromhex("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")4PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"Die gleichen Variablen, die wir in Schritt 4 verwendet haben.
1WETH_TRADE_AMOUNT=1Der zu handelnde Betrag.
1ERC20_ABI = [2 { "name": "symbol", ... },3 { "name": "decimals", ... },4 { "name": "balanceOf", ...},5 { "name": "approve", ...}6]Um tatsächlich zu handeln, benötigen wir die Funktion approve. Wir möchten auch die Salden davor und danach anzeigen, also benötigen wir auch balanceOf.
1SWAP_ROUTER_ABI = [2 { "name": "exactInput", ...},3]In der SwapRouter-ABI benötigen wir nur exactInput. Es gibt eine verwandte Funktion, exactOutput, die wir verwenden könnten, um genau ein WETH zu kaufen, aber der Einfachheit halber verwenden wir in beiden Fällen nur exactInput.
1account = w3.eth.account.from_key(PRIVATE_KEY)2swap_router = w3.eth.contract(3 address=SWAP_ROUTER_ADDRESS,4 abi=SWAP_ROUTER_ABI5)Die Web3-Definitionen für das account (opens in a new tab) (Konto) und den SwapRouter-Vertrag.
1def txn_params() -> dict:2 return {3 "from": account.address,4 "value": 0,5 "gas": 300000,6 "nonce": w3.eth.get_transaction_count(account.address),7 }Die Transaktionsparameter. Wir benötigen hier eine Funktion, da sich die Nonce (opens in a new tab) jedes Mal ändern muss.
1def approve_token(contract: Contract, amount: int):Genehmigen Sie eine Token-Freigabe für SwapRouter.
1 txn = contract.functions.approve(SWAP_ROUTER_ADDRESS, amount).build_transaction(txn_params())2 signed_txn = w3.eth.account.sign_transaction(txn, private_key=PRIVATE_KEY)3 tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction)So senden wir eine Transaktion in Web3. Zuerst verwenden wir das Contract-Objekt (opens in a new tab), um die Transaktion aufzubauen. 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.
1 print(f"Approve transaction sent: {tx_hash.hex()}")2 w3.eth.wait_for_transaction_receipt(tx_hash)3 print("Approve transaction mined.")w3.eth.wait_for_transaction_receipt (opens in a new tab) wartet, bis die Transaktion gemint wurde. Es gibt bei Bedarf den Beleg zurück.
1SELL_PARAMS = {2 "path": WETH_TO_USDC,3 "recipient": account.address,4 "deadline": 2**256 - 1,5 "amountIn": WETH_TRADE_AMOUNT * 10 ** wethusdc_pool.token1.decimals,6 "amountOutMinimum": 0,7}Dies sind die Parameter beim Verkauf von WETH.
1def make_buy_params(quote: Quote) -> dict:2 return {3 "path": USDC_TO_WETH,4 "recipient": account.address,5 "deadline": 2**256 - 1,6 "amountIn": int(quote.price*WETH_TRADE_AMOUNT) * 10**wethusdc_pool.token0.decimals,7 "amountOutMinimum": 0,8 }Im Gegensatz zu SELL_PARAMS können sich die Kaufparameter ändern. Der Eingabebetrag sind die Kosten für 1 WETH, wie in quote verfügbar.
1def buy(quote: Quote):2 buy_params = make_buy_params(quote)3 approve_token(wethusdc_pool.token0.contract, buy_params["amountIn"])4 txn = swap_router.functions.exactInput(buy_params).build_transaction(txn_params())5 signed_txn = w3.eth.account.sign_transaction(txn, private_key=PRIVATE_KEY)6 tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction)7 print(f"Buy transaction sent: {tx_hash.hex()}")8 w3.eth.wait_for_transaction_receipt(tx_hash)9 print("Buy transaction mined.")10
11
12def sell():13 approve_token(wethusdc_pool.token1.contract,14 WETH_TRADE_AMOUNT * 10**wethusdc_pool.token1.decimals)15 txn = swap_router.functions.exactInput(SELL_PARAMS).build_transaction(txn_params())16 signed_txn = w3.eth.account.sign_transaction(txn, private_key=PRIVATE_KEY)17 tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction)18 print(f"Sell transaction sent: {tx_hash.hex()}")19 w3.eth.wait_for_transaction_receipt(tx_hash)20 print("Sell transaction mined.")Die Funktionen buy() und sell() sind nahezu identisch. Zuerst genehmigen wir eine ausreichende Freigabe für SwapRouter, und dann rufen wir ihn mit dem korrekten Pfad und Betrag auf.
1def balances():2 token0_balance = wethusdc_pool.token0.contract.functions.balanceOf(account.address).call()3 token1_balance = wethusdc_pool.token1.contract.functions.balanceOf(account.address).call()4
5 print(f"{wethusdc_pool.token0.symbol} Balance: {Decimal(token0_balance) / Decimal(10 ** wethusdc_pool.token0.decimals)}")6 print(f"{wethusdc_pool.token1.symbol} Balance: {Decimal(token1_balance) / Decimal(10 ** wethusdc_pool.token1.decimals)}")Berichten Sie die Benutzersalden in beiden Währungen.
1print("Account balances before trade:")2balances()3
4if (expected_price > current_price):5 print(f"Buy, I expect the price to go up by {expected_price - current_price} USD")6 buy(wethusdc_quotes[-1])7else:8 print(f"Sell, I expect the price to go down by {current_price - expected_price} USD")9 sell()10
11print("Account balances after trade:")12balances()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 indem Sie 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 Verä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, ist es wahrscheinlich sinnvoll, 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 Handelsstrategie geheim halten möchten?
KI-Anbieter können die Abfragen sehen, die Sie an ihre LLMs senden, was das geniale Handelssystem offenlegen könnte, das Sie mit Ihrem Agenten entwickelt haben. Ein Handelssystem, das zu viele Menschen nutzen, ist wertlos, da zu viele Menschen versuchen zu kaufen, wenn Sie kaufen möchten (und der Preis steigt), und versuchen zu verkaufen, wenn Sie verkaufen möchten (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 eine 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 Glück!
Weitere meiner Arbeiten finden Sie hier (opens in a new tab).
Letzte Aktualisierung der Seite: 3. März 2026