Crea il tuo agente di trading IA su Ethereum
In questo tutorial imparerai come costruire un semplice agente di trading IA. Questo agente funziona seguendo questi passaggi:
- Leggere i prezzi attuali e passati di un token, oltre ad altre informazioni potenzialmente rilevanti
- Costruire una query con queste informazioni, insieme a informazioni di base per spiegare come potrebbero essere rilevanti
- Inviare la query e ricevere in risposta un prezzo previsto
- Fare trading in base alla raccomandazione
- Attendere e ripetere
Questo agente dimostra come leggere informazioni, tradurle in una query che produce una risposta utilizzabile e utilizzare tale risposta. Tutti questi sono passaggi richiesti per un agente IA. Questo agente è implementato in Python perché è il linguaggio più comune utilizzato nell'IA.
Perché farlo?
Gli agenti di trading automatizzati consentono agli sviluppatori di selezionare ed eseguire una strategia di trading. Gli agenti IA consentono strategie di trading più complesse e dinamiche, utilizzando potenzialmente informazioni e algoritmi che lo sviluppatore non ha nemmeno preso in considerazione.
Gli strumenti
Questo tutorial utilizza Python (opens in a new tab), la libreria Web3 (opens in a new tab) e Uniswap v3 (opens in a new tab) per le quotazioni e il trading.
Perché Python?
Il linguaggio più utilizzato per l'IA è Python (opens in a new tab), quindi lo useremo qui. Non preoccuparti se non conosci Python. Il linguaggio è molto chiaro e spiegherò esattamente cosa fa.
La libreria Web3 (opens in a new tab) è l'API Ethereum per Python più comune. È piuttosto facile da usare.
Fare trading sulla blockchain
Ci sono molti exchange decentralizzati (DEX) che ti permettono di scambiare token su Ethereum. Tuttavia, tendono ad avere tassi di cambio simili a causa dell'arbitraggio.
Uniswap (opens in a new tab) è un DEX ampiamente utilizzato che possiamo usare sia per le quotazioni (per vedere i valori relativi dei token) sia per gli scambi.
OpenAI
Per un modello linguistico di grandi dimensioni (LLM), ho scelto di iniziare con OpenAI (opens in a new tab). Per eseguire l'applicazione in questo tutorial dovrai pagare per l'accesso all'API. Il pagamento minimo di 5$ è più che sufficiente.
Sviluppo, passo dopo passo
Per semplificare lo sviluppo, procederemo per fasi. Ogni passaggio è un branch su GitHub.
Per iniziare
Ci sono dei passaggi per iniziare su UNIX o Linux (incluso WSL (opens in a new tab))
-
Se non lo hai già, scarica e installa Python (opens in a new tab).
-
Clona il repository GitHub.
1git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started2cd 260215-ai-agent
1
23. Installa [`uv`](https://docs.astral.sh/uv/getting-started/installation/). Il comando sul tuo sistema potrebbe essere diverso.3
4 ```sh5 pipx install uv-
Scarica le librerie.
1uv sync
1
25. Attiva l'ambiente virtuale.3
4 ```sh5 source .venv/bin/activate-
Per verificare che Python e Web3 funzionino correttamente, esegui
python3e forniscigli questo programma. Puoi inserirlo al prompt>>>; non c'è bisogno di creare un file.1from web3 import Web32MAINNET_URL = "https://eth.drpc.org"3w3 = Web3(Web3.HTTPProvider(MAINNET_URL))4w3.eth.block_number5quit()
1
2### Leggere dalla blockchain \{#read-blockchain\}3
4Il passaggio successivo è leggere dalla blockchain. Per farlo, devi passare al branch `02-read-quote` e poi usare `uv` per eseguire il programma.5
6```sh7git checkout 02-read-quote8uv run agent.pyDovresti ricevere un elenco di oggetti Quote, ciascuno con un timestamp, un prezzo e l'asset (attualmente sempre WETH/USDC).
Ecco una spiegazione riga per riga.
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 sysImporta le librerie di cui abbiamo bisogno. Sono spiegate di seguito quando vengono utilizzate.
1print = functools.partial(print, flush=True)Sostituisce il print di Python con una versione che svuota sempre l'output immediatamente. Questo è utile in uno script a esecuzione prolungata perché non vogliamo aspettare per gli aggiornamenti di stato o l'output di debug.
1MAINNET_URL = "https://eth.drpc.org"Un URL per accedere alla rete principale. Puoi ottenerne uno da Nodo come servizio o usarne uno tra quelli pubblicizzati su Chainlist (opens in a new tab).
1BLOCK_TIME_SECONDS = 122MINUTE_BLOCKS = int(60 / BLOCK_TIME_SECONDS)3HOUR_BLOCKS = MINUTE_BLOCKS * 604DAY_BLOCKS = HOUR_BLOCKS * 24Un blocco della rete principale di Ethereum viene tipicamente prodotto ogni dodici secondi, quindi questo è il numero di blocchi che ci aspetteremmo in un determinato periodo di tempo. Nota che questa non è una cifra esatta. Quando il proponente del blocco è inattivo, quel blocco viene saltato e il tempo per il blocco successivo è di 24 secondi. Se volessimo ottenere il blocco esatto per un timestamp, useremmo la ricerca binaria (opens in a new tab). Tuttavia, questo è abbastanza vicino per i nostri scopi. Prevedere il futuro non è una scienza esatta.
1CYCLE_BLOCKS = DAY_BLOCKSLa dimensione del ciclo. Esaminiamo le quotazioni una volta per ciclo e cerchiamo di stimare il valore alla fine del ciclo successivo.
1# L'indirizzo della pool che stiamo leggendo2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")I valori delle quotazioni sono presi dalla pool USDC/WETH di Uniswap 3 all'indirizzo 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 (opens in a new tab). Questo indirizzo è già in formato checksum, ma è meglio usare Web3.to_checksum_address (opens in a new tab) per rendere il codice riutilizzabile.
1POOL_ABI = [2 { "name": "slot0", ... },3 { "name": "token0", ... },4 { "name": "token1", ... },5]6
7ERC20_ABI = [8 { "name": "symbol", ... },9 { "name": "decimals", ... }10]Queste sono le ABI (opens in a new tab) per i due contratti che dobbiamo contattare. Per mantenere il codice conciso, includiamo solo le funzioni che dobbiamo chiamare.
1w3 = Web3(Web3.HTTPProvider(MAINNET_URL))Inizializza la libreria Web3 (opens in a new tab) e connettiti a un nodo Ethereum.
1@dataclass(frozen=True)2class ERC20Token:3 address: str4 symbol: str5 decimals: int6 contract: ContractQuesto è un modo per creare una classe di dati (data class) in Python. Il tipo di dati Contract (opens in a new tab) viene utilizzato per connettersi al contratto. Nota il (frozen=True). In Python i booleani (opens in a new tab) sono definiti come True o False, con l'iniziale maiuscola. Questa classe di dati è frozen (congelata), il che significa che i campi non possono essere modificati.
Nota l'indentazione. A differenza dei linguaggi derivati dal C (opens in a new tab), Python usa l'indentazione per denotare i blocchi. L'interprete Python sa che la definizione seguente non fa parte di questa classe di dati perché non inizia con la stessa indentazione dei campi della classe di dati.
1@dataclass(frozen=True)2class PoolInfo:3 address: str4 token0: ERC20Token5 token1: ERC20Token6 contract: Contract7 asset: str8 decimal_factor: Decimal = 1Il tipo Decimal (opens in a new tab) viene utilizzato per gestire accuratamente le frazioni decimali.
1 def get_price(self, block: int) -> Decimal:Questo è il modo per definire una funzione in Python. La definizione è indentata per mostrare che fa ancora parte di PoolInfo.
In una funzione che fa parte di una classe di dati, il primo parametro è sempre self, l'istanza della classe di dati che ha effettuato la chiamata. Qui c'è un altro parametro, il numero del blocco.
1 assert block <= w3.eth.block_number, "Block is in the future"Se potessimo leggere il futuro, non avremmo bisogno dell'IA per il trading.
1 sqrt_price_x96 = Decimal(self.contract.functions.slot0().call(block_identifier=block)[0])La sintassi per chiamare una funzione sull'EVM da Web3 è questa: <contract object>.functions.<function name>().call(<parameters>). I parametri possono essere i parametri della funzione EVM (se presenti; qui non ce ne sono) o parametri nominati (opens in a new tab) per modificare il comportamento della blockchain. Qui ne usiamo uno, block_identifier, per specificare il numero del blocco in cui desideriamo eseguire.
Il risultato è questa struct, in forma di array (opens in a new tab). Il primo valore è una funzione del tasso di cambio tra i due token.
1 raw_price = (sqrt_price_x96 / Decimal(2**96)) ** 2Per ridurre i calcoli on-chain, Uniswap v3 non memorizza il fattore di cambio effettivo, ma piuttosto la sua radice quadrata. Poiché l'EVM non supporta la matematica in virgola mobile o le frazioni, invece del valore effettivo, la risposta è
1 # (token1 per token0)2 return 1/(raw_price * self.decimal_factor)Il prezzo grezzo che otteniamo è il numero di token0 che riceviamo per ogni token1. Nella nostra pool token0 è USDC (stablecoin con lo stesso valore di un dollaro USA) e token1 è WETH (opens in a new tab). Il valore che vogliamo veramente è il numero di dollari per WETH, non l'inverso.
Il fattore decimale è il rapporto tra i fattori decimali (opens in a new tab) per i due token.
1@dataclass(frozen=True)2class Quote:3 timestamp: str4 price: Decimal5 asset: strQuesta classe di dati rappresenta una quotazione: il prezzo di un asset specifico in un dato momento. A questo punto, il campo asset è irrilevante perché usiamo una singola pool e quindi abbiamo un singolo asset. Tuttavia, aggiungeremo altri asset in seguito.
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 )Questa funzione accetta un indirizzo e restituisce informazioni sul contratto del token a quell'indirizzo. Per creare un nuovo Contract Web3 (opens in a new tab), forniamo l'indirizzo e l'ABI a 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 )Questa funzione restituisce tutto ciò di cui abbiamo bisogno su una pool specifica (opens in a new tab). La sintassi f"<string>" è una stringa formattata (opens in a new tab).
1def get_quote(pool: PoolInfo, block_number: int = None) -> Quote:Ottieni un oggetto Quote. Il valore predefinito per block_number è None (nessun valore).
1 if block_number is None:2 block_number = w3.eth.block_numberSe non è stato specificato un numero di blocco, usa w3.eth.block_number, che è l'ultimo numero di blocco. Questa è la sintassi per un'istruzione if (opens in a new tab).
Potrebbe sembrare che sarebbe stato meglio impostare semplicemente il valore predefinito su w3.eth.block_number, ma non funziona bene perché sarebbe il numero del blocco al momento in cui la funzione viene definita. In un agente a esecuzione prolungata, questo sarebbe un problema.
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 )Usa la libreria datetime (opens in a new tab) per formattarlo in un formato leggibile per gli esseri umani e per i modelli linguistici di grandi dimensioni (LLM). Usa Decimal.quantize (opens in a new tab) per arrotondare il valore a due cifre decimali.
1def get_quotes(pool: PoolInfo, start_block: int, end_block: int, step: int) -> list[Quote]:In Python definisci una lista (opens in a new tab) che può contenere solo un tipo specifico usando list[<type>].
1 quotes = []2 for block in range(start_block, end_block + 1, step):In Python un ciclo for (opens in a new tab) tipicamente itera su una lista. L'elenco dei numeri di blocco in cui trovare le quotazioni proviene da range (opens in a new tab).
1 quote = get_quote(pool, block)2 quotes.append(quote)3 return quotesPer ogni numero di blocco, ottieni un oggetto Quote e aggiungilo alla lista quotes. Quindi restituisci quella lista.
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)Questo è il codice principale dello script. Leggi le informazioni sulla pool, ottieni dodici quotazioni e stampale con pprint (opens in a new tab).
Creare un prompt
Successivamente, dobbiamo convertire questo elenco di quotazioni in un prompt per un LLM e ottenere un valore futuro atteso.
1git checkout 03-create-prompt2uv run agent.pyL'output ora sarà un prompt per un LLM, simile a:
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.Nota che qui ci sono quotazioni per due asset, WETH/USDC e WBTC/WETH. L'aggiunta di quotazioni da un altro asset potrebbe migliorare l'accuratezza della previsione.
Come si presenta un prompt
Questo prompt contiene tre sezioni, che sono piuttosto comuni nei prompt degli LLM.
-
Informazioni. Gli LLM hanno molte informazioni derivanti dal loro addestramento, ma di solito non hanno le più recenti. Questo è il motivo per cui dobbiamo recuperare le ultime quotazioni qui. L'aggiunta di informazioni a un prompt è chiamata retrieval augmented generation (RAG) (opens in a new tab).
-
La domanda vera e propria. Questo è ciò che vogliamo sapere.
-
Istruzioni per la formattazione dell'output. Normalmente, un LLM ci darà una stima con una spiegazione di come ci è arrivato. Questo è meglio per gli esseri umani, ma un programma per computer ha solo bisogno del risultato finale.
Spiegazione del codice
Ecco il nuovo codice.
1from datetime import datetime, timezone, timedeltaDobbiamo fornire all'LLM il momento per il quale vogliamo una stima. Per ottenere un tempo "n minuti/ore/giorni" nel futuro, usiamo la classe timedelta (opens in a new tab).
1# Gli indirizzi delle pool che stiamo leggendo2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")3WETHWBTC_ADDRESS = Web3.to_checksum_address("0xCBCdF9626bC03E24f779434178A73a0B4bad62eD")Abbiamo due pool che dobbiamo leggere.
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 per token0)12 if self.reverse:13 return 1/(raw_price * self.decimal_factor)14 else:15 return raw_price * self.decimal_factorNella pool WETH/USDC, vogliamo sapere quanti token0 (USDC) ci servono per comprare un token1 (WETH). Nella pool WETH/WBTC, vogliamo sapere quanti token1 (WETH) ci servono per comprare un token0 (WBTC, che è Bitcoin avvolto). Dobbiamo tenere traccia se il rapporto della pool deve essere invertito.
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 )Per sapere se una pool deve essere invertita, dobbiamo ottenerlo come input per read_pool. Inoltre, il simbolo dell'asset deve essere impostato correttamente.
La sintassi <a> if <b> else <c> è l'equivalente Python dell'operatore condizionale ternario (opens in a new tab), che in un linguaggio derivato dal C sarebbe <b> ? <a> : <c>.
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 resultQuesta funzione costruisce una stringa che formatta un elenco di oggetti Quote, supponendo che si applichino tutti allo stesso asset.
1def make_prompt(quotes: list[list[Quote]], expected_time: str, asset: str) -> str:2 return f"""In Python le stringhe letterali multilinea (opens in a new tab) sono scritte come """ .... """.
1Given these quotes:2{3 functools.reduce(lambda acc, q: acc + '\n' + q,4 map(lambda q: format_quotes(q), quotes))5}Qui, usiamo il pattern MapReduce (opens in a new tab) per generare una stringa per ogni elenco di quotazioni con format_quotes, quindi le riduciamo in una singola stringa da usare nel 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 """Il resto del prompt è come previsto.
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)Esamina le due pool e ottieni le quotazioni da entrambe.
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))Determina il momento futuro per il quale vogliamo la stima e crea il prompt.
Interfacciarsi con un LLM
Successivamente, interroghiamo un vero LLM e riceviamo un valore futuro atteso. Ho scritto questo programma usando OpenAI, quindi se vuoi usare un provider diverso, dovrai adattarlo.
-
Ottieni un account OpenAI (opens in a new tab)
-
Finanzia l'account (opens in a new tab): l'importo minimo al momento della stesura è di 5$
-
Nella riga di comando, esporta la chiave API in modo che il tuo programma possa usarla
1export OPENAI_API_KEY=sk-<the rest of the key goes here>
1
25. Fai il checkout ed esegui l'agente3
4 ```sh5 git checkout 04-interface-llm6 uv run agent.pyEcco il nuovo codice.
1from openai import OpenAI2
3open_ai = OpenAI() # Il client legge la variabile d'ambiente OPENAI_API_KEYImporta e istanzia l'API di OpenAI.
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)Chiama l'API di OpenAI (open_ai.chat.completions.create) per creare la risposta.
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")Emetti il prezzo e fornisci una raccomandazione di acquisto o vendita.
Testare le previsioni
Ora che possiamo generare previsioni, possiamo anche usare i dati storici per valutare se produciamo previsioni utili.
1uv run test-predictor.pyIl risultato atteso è simile a:
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%La maggior parte del tester è identica all'agente, ma ecco le parti nuove o modificate.
1CYCLES_FOR_TEST = 40 # Per il backtest, su quanti cicli effettuiamo il test2
3# Ottieni molte quotazioni4wethusdc_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)Guardiamo indietro di CYCLES_FOR_TEST (specificato qui come 40) giorni.
1# Crea previsioni e verificale con lo storico reale2
3total_error = Decimal(0)4changes = []Ci sono due tipi di errori a cui siamo interessati. Il primo, total_error, è semplicemente la somma degli errori commessi dal predittore.
Per capire il secondo, changes, dobbiamo ricordare lo scopo dell'agente. Non è prevedere il rapporto WETH/USDC (prezzo di ETH). È emettere raccomandazioni di vendita e acquisto. Se il prezzo è attualmente di 2000$ e prevede 2010$ per domani, non ci dispiace se il risultato effettivo è 2020$ e guadagniamo soldi extra. Ma ci dispiace se ha previsto 2010$ e ha comprato ETH in base a quella raccomandazione, e il prezzo scende a 1990$.
1for index in range(0,len(wethusdc_quotes)-CYCLES_BACK):Possiamo esaminare solo i casi in cui è disponibile la cronologia completa (i valori utilizzati per la previsione e il valore reale con cui confrontarla). Ciò significa che il caso più recente deve essere quello iniziato CYCLES_BACK fa.
1 wethusdc_slice = wethusdc_quotes[index:index+CYCLES_BACK]2 wethwbtc_slice = wethwbtc_quotes[index:index+CYCLES_BACK]Usa gli slice (opens in a new tab) per ottenere lo stesso numero di campioni del numero utilizzato dall'agente. Il codice tra qui e il segmento successivo è lo stesso codice per ottenere una previsione che abbiamo nell'agente.
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].priceOttieni il prezzo previsto, il prezzo reale e il prezzo al momento della previsione. Abbiamo bisogno del prezzo al momento della previsione per determinare se la raccomandazione era di comprare o vendere.
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")Calcola l'errore e aggiungilo al totale.
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)Per changes, vogliamo l'impatto monetario dell'acquisto o della vendita di un ETH. Quindi, per prima cosa, dobbiamo determinare la raccomandazione, quindi valutare come è cambiato il prezzo effettivo e se la raccomandazione ha fatto guadagnare (variazione positiva) o perdere denaro (variazione negativa).
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")Riporta i risultati.
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%}")Usa filter (opens in a new tab) per contare il numero di giorni redditizi e il numero di giorni in perdita. Il risultato è un oggetto filtro, che dobbiamo convertire in una lista per ottenerne la lunghezza.
Inviare transazioni
Ora dobbiamo effettivamente inviare transazioni. Tuttavia, non voglio spendere soldi veri a questo punto, prima che il sistema sia collaudato. Invece, creeremo una biforcazione locale della rete principale e faremo "trading" su quella rete.
Ecco i passaggi per creare una biforcazione locale e abilitare il trading.
-
Installa Foundry (opens in a new tab)
-
Avvia
anvil(opens in a new tab)1anvil --fork-url https://eth.drpc.org --block-time 12
1
2 `anvil` è in ascolto sull'URL predefinito per Foundry, http://localhost:8545, quindi non abbiamo bisogno di specificare l'URL per [il comando `cast`](https://getfoundry.sh/cast/overview) che usiamo per manipolare la blockchain.3
43. Quando si esegue in `anvil`, ci sono dieci account di test che hanno ETH: imposta le variabili d'ambiente per il primo5
6 ```sh7 PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff808 ADDRESS=`cast wallet address $PRIVATE_KEY`-
Questi sono i contratti che dobbiamo usare.
SwapRouter(opens in a new tab) è il contratto Uniswap v3 che usiamo per fare effettivamente trading. Potremmo fare trading direttamente tramite la pool, ma questo è molto più semplice.Le due variabili in basso sono i percorsi di Uniswap v3 necessari per lo scambio tra WETH e USDC.
1WETH_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc22USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f56404SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C058615645WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB486USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
1
25. Ciascuno degli account di test ha 10.000 ETH. Usa il contratto WETH per avvolgere 1000 ETH per ottenere 1000 WETH per il trading.3
4 ```sh5 cast send $WETH_ADDRESS "deposit()" --value 1000ether --private-key $PRIVATE_KEY-
Usa
SwapRouterper scambiare 500 WETH con USDC.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 La chiamata `approve` crea un'indennità (allowance) che consente a `SwapRouter` di spendere alcuni dei nostri token. I contratti non possono monitorare gli eventi, quindi se trasferissimo i token direttamente al contratto `SwapRouter`, non saprebbe di essere stato pagato. Invece, permettiamo al contratto `SwapRouter` di spendere un certo importo, e poi `SwapRouter` lo fa. Questo viene fatto tramite una funzione chiamata da `SwapRouter`, in modo che sappia se ha avuto successo.3
47. Verifica di avere abbastanza di entrambi i token.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 | bcOra che abbiamo WETH e USDC, possiamo effettivamente eseguire l'agente.
1git checkout 05-trade2uv run agent.pyL'output sarà simile a:
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: 499Per usarlo effettivamente, hai bisogno di alcune piccole modifiche.
- Alla riga 14, cambia
MAINNET_URLin un vero punto di accesso, comehttps://eth.drpc.org - Alla riga 28, cambia
PRIVATE_KEYcon la tua chiave privata - A meno che tu non sia molto ricco e possa comprare o vendere 1 ETH ogni giorno per un agente non collaudato, potresti voler cambiare la riga 29 per diminuire
WETH_TRADE_AMOUNT
Spiegazione del codice
Ecco il nuovo codice.
1SWAP_ROUTER_ADDRESS=Web3.to_checksum_address("0xE592427A0AEce92De3Edee1F18E0157C05861564")2WETH_TO_USDC=bytes.fromhex("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")3USDC_TO_WETH=bytes.fromhex("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")4PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"Le stesse variabili che abbiamo usato nel passaggio 4.
1WETH_TRADE_AMOUNT=1L'importo da scambiare.
1ERC20_ABI = [2 { "name": "symbol", ... },3 { "name": "decimals", ... },4 { "name": "balanceOf", ...},5 { "name": "approve", ...}6]Per fare effettivamente trading, abbiamo bisogno della funzione approve. Vogliamo anche mostrare i saldi prima e dopo, quindi abbiamo bisogno anche di balanceOf.
1SWAP_ROUTER_ABI = [2 { "name": "exactInput", ...},3]Nell'ABI di SwapRouter abbiamo solo bisogno di exactInput. C'è una funzione correlata, exactOutput, che potremmo usare per comprare esattamente un WETH, ma per semplicità usiamo solo exactInput in entrambi i casi.
1account = w3.eth.account.from_key(PRIVATE_KEY)2swap_router = w3.eth.contract(3 address=SWAP_ROUTER_ADDRESS,4 abi=SWAP_ROUTER_ABI5)Le definizioni Web3 per l'account (opens in a new tab) e il contratto SwapRouter.
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 }I parametri della transazione. Abbiamo bisogno di una funzione qui perché il nonce (opens in a new tab) deve cambiare ogni volta.
1def approve_token(contract: Contract, amount: int):Approva un'indennità di token per 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)Questo è il modo in cui inviamo una transazione in Web3. Per prima cosa usiamo l'oggetto Contract (opens in a new tab) per costruire la transazione. Quindi usiamo web3.eth.account.sign_transaction (opens in a new tab) per firmare la transazione, usando PRIVATE_KEY. Infine, usiamo w3.eth.send_raw_transaction (opens in a new tab) per inviare la transazione.
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) attende fino a quando la transazione non viene minata. Restituisce la ricevuta se necessario.
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}Questi sono i parametri quando si vende 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 }A differenza di SELL_PARAMS, i parametri di acquisto possono cambiare. L'importo di input è il costo di 1 WETH, come disponibile in quote.
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.")Le funzioni buy() e sell() sono quasi identiche. Per prima cosa approviamo un'indennità sufficiente per SwapRouter, e poi lo chiamiamo con il percorso e l'importo corretti.
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)}")Riporta i saldi dell'utente in entrambe le valute.
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()Questo agente attualmente funziona solo una volta. Tuttavia, puoi modificarlo per farlo funzionare continuamente eseguendolo da crontab (opens in a new tab) o racchiudendo le righe 368-400 in un ciclo e usando time.sleep (opens in a new tab) per attendere fino al momento del ciclo successivo.
Possibili miglioramenti
Questa non è una versione di produzione completa; è semplicemente un esempio per insegnare le basi. Ecco alcune idee per miglioramenti.
Trading più intelligente
Ci sono due fatti importanti che l'agente ignora quando decide cosa fare.
- L'entità del cambiamento previsto. L'agente vende un importo fisso di
WETHse si prevede che il prezzo diminuirà, indipendentemente dall'entità del calo. Probabilmente, sarebbe meglio ignorare i cambiamenti minori e vendere in base a quanto ci aspettiamo che il prezzo diminuisca. - Il portafoglio attuale. Se il 10% del tuo portafoglio è in WETH e pensi che il prezzo salirà, probabilmente ha senso comprarne di più. Ma se il 90% del tuo portafoglio è in WETH, potresti essere sufficientemente esposto e non c'è bisogno di comprarne di più. Il contrario è vero se ti aspetti che il prezzo scenda.
E se volessi mantenere segreta la tua strategia di trading?
I fornitori di IA possono vedere le query che invii ai loro LLM, il che potrebbe esporre il geniale sistema di trading che hai sviluppato con il tuo agente. Un sistema di trading che troppe persone usano è inutile perché troppe persone cercano di comprare quando tu vuoi comprare (e il prezzo sale) e cercano di vendere quando tu vuoi vendere (e il prezzo scende).
Puoi eseguire un LLM localmente, ad esempio, usando LM-Studio (opens in a new tab), per evitare questo problema.
Da bot IA ad agente IA
Si può sostenere a ragione che questo sia un bot IA, non un agente IA. Implementa una strategia relativamente semplice che si basa su informazioni predefinite. Possiamo abilitare l'auto-miglioramento, ad esempio, fornendo un elenco di pool Uniswap v3 e i loro ultimi valori e chiedendo quale combinazione abbia il miglior valore predittivo.
Protezione dallo slippage
Attualmente non c'è protezione dallo slippage (opens in a new tab). Se la quotazione attuale è di 2000$ e il prezzo previsto è di 2100$, l'agente comprerà. Tuttavia, se prima che l'agente compri il costo sale a 2200$, non ha più senso comprare.
Per implementare la protezione dallo slippage, specifica un valore amountOutMinimum nelle righe 325 e 334 di agent.py (opens in a new tab).
Conclusione
Speriamo che ora tu ne sappia abbastanza per iniziare con gli agenti IA. Questa non è una panoramica completa dell'argomento; ci sono interi libri dedicati a questo, ma è sufficiente per iniziare. Buona fortuna!
Vedi qui per altri miei lavori (opens in a new tab).
Ultimo aggiornamento della pagina: 3 marzo 2026