Vai al contenuto principale

Crea il tuo agente di trading IA su Ethereum

IA
trading
agente
python
Intermedio
Ori Pomerantz
13 febbraio 2026
24 minuti letti

In questa guida imparerai a creare un semplice agente di trading IA. Questo agente funziona seguendo questi passaggi:

  1. Leggere i prezzi attuali e passati di un token, nonché altre informazioni potenzialmente pertinenti
  2. Costruire una query con queste informazioni, insieme a informazioni di base per spiegare come potrebbero essere pertinenti
  3. Inviare la query e ricevere in risposta un prezzo previsto
  4. Effettuare uno scambio in base alla raccomandazione
  5. Attendere e ripetere

Questo agente dimostra come leggere le informazioni, tradurle in una query che fornisce 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 considerato di usare.

Gli strumenti

Questa guida utilizza Python (opens in a new tab), la libreria Web3 (opens in a new tab) e Uniswap v3 (opens in a new tab) per quotazioni e trading.

Perché Python?

Il linguaggio più utilizzato per l'IA è Python (opens in a new tab), quindi lo usiamo qui. Non ti preoccupare se non conosci Python. Il linguaggio è molto chiaro e spiegherò esattamente cosa fa.

La libreria Web3 (opens in a new tab) è l'API Python di Ethereum più comune. È abbastanza facile da usare.

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, ho scelto di iniziare con OpenAI (opens in a new tab). Per eseguire l'applicazione in questa guida dovrai pagare per l'accesso all'API. Il pagamento minimo di 5$ è più che sufficiente.

Sviluppo, passo dopo passo

Per semplificare lo sviluppo, procediamo per fasi. Ogni passaggio è un branch in GitHub.

Per iniziare

Ci sono passaggi per iniziare con UNIX o Linux (incluso WSL (opens in a new tab))

  1. Se non lo hai già, scarica e installa Python (opens in a new tab).

  2. Clona la repository di GitHub.

    1git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started
    2cd 260215-ai-agent
  3. Installa uv (opens in a new tab). Il comando sul tuo sistema potrebbe essere diverso.

    1pipx install uv
  4. Scarica le librerie.

    1uv sync
  5. Attiva l'ambiente virtuale.

    1source .venv/bin/activate
  6. Per verificare che Python e Web3 funzionino correttamente, esegui python3 e forniscigli questo programma. Puoi inserirlo al prompt >>>; non è necessario creare un file.

    1from web3 import Web3
    2MAINNET_URL = "https://eth.drpc.org"
    3w3 = Web3(Web3.HTTPProvider(MAINNET_URL))
    4w3.eth.block_number
    5quit()

Lettura dalla blockchain

Il passo successivo è leggere dalla blockchain. Per farlo, devi passare al branch 02-read-quote e quindi usare uv per eseguire il programma.

1git checkout 02-read-quote
2uv run agent.py

Dovresti ricevere un elenco di oggetti Quote, ognuno con un indicatore ora, un prezzo e l'asset (attualmente sempre WETH/USDC).

Ecco una spiegazione riga per riga.

1from web3 import Web3
2from web3.contract import Contract
3from decimal import Decimal, ROUND_HALF_UP
4from dataclasses import dataclass
5from datetime import datetime, timezone
6from pprint import pprint
7import time
8import functools
9import sys
Mostra tutto

Importa le librerie di cui abbiamo bisogno. Sono spiegati di seguito quando vengono utilizzati.

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 lunga esecuzione perché non vogliamo attendere aggiornamenti di stato o output di debug.

1MAINNET_URL = "https://eth.drpc.org"

Un URL per accedere alla Rete Principale. Puoi ottenerne uno da Nodo come servizio o usare uno di quelli pubblicizzati in Chainlist (opens in a new tab).

1BLOCK_TIME_SECONDS = 12
2MINUTE_BLOCKS = int(60 / BLOCK_TIME_SECONDS)
3HOUR_BLOCKS = MINUTE_BLOCKS * 60
4DAY_BLOCKS = HOUR_BLOCKS * 24

Un blocco della Rete Principale di Ethereum si verifica in genere ogni dodici secondi, quindi questo è il numero di blocchi che ci aspetteremmo si verifichino in un periodo di tempo. Nota che questa non è una cifra esatta. Quando il proponente del blocco non è attivo, quel blocco viene saltato e il tempo per il blocco successivo è di 24 secondi. Se volessimo ottenere il blocco esatto per un indicatore ora, useremmo la ricerca binaria (opens in a new tab). Tuttavia, questo è sufficientemente vicino per i nostri scopi. Prevedere il futuro non è una scienza esatta.

1CYCLE_BLOCKS = DAY_BLOCKS

La dimensione del ciclo. Esaminiamo le quotazioni una volta per ciclo e proviamo a stimare il valore alla fine del ciclo successivo.

1# L'indirizzo del gruppo che stiamo leggendo
2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")

I valori delle quotazioni sono presi dal gruppo 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]
Mostra tutto

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: str
4 symbol: str
5 decimals: int
6 contract: Contract

Questo è un modo per creare una classe di dati 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 la lettera maiuscola. Questa classe di dati è frozen, 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 seguente definizione 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: str
4 token0: ERC20Token
5 token1: ERC20Token
6 contract: Contract
7 asset: str
8 decimal_factor: Decimal = 1

Il 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 chiamato qui. Qui c'è un altro parametro, il numero del blocco.

1 assert block <= w3.eth.block_number, "Il blocco è nel futuro"

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 sulla EVM da Web3 è questa: <oggetto contratto>.functions.<nome funzione>().call(<parametri>). I parametri possono essere i parametri della funzione EVM (se presenti; qui non ce ne sono) o parametri nominativi (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)) ** 2

Per ridurre i calcoli sulla catena, Uniswap v3 non memorizza il fattore di cambio effettivo ma la sua radice quadrata. Poiché l'EVM non supporta la matematica in virgola mobile o le frazioni, invece del valore effettivo, la risposta è price&#x22C5296

1 # (token1 per token0)
2 return 1/(raw_price * self.decimal_factor)

Il prezzo grezzo che otteniamo è il numero di token0 che otteniamo per ogni token1. Nel nostro gruppo 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: str
4 price: Decimal
5 asset: str

Questa 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 un singolo gruppo 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=token
11 )
Mostra tutto

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 )
Mostra tutto

Questa funzione restituisce tutto ciò di cui abbiamo bisogno su un gruppo specifico (opens in a new tab). La sintassi f"<stringa>" è 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_number

Se 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 di blocco al momento della definizione della funzione. In un agente a lunga esecuzione, 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.asset
7 )

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 si definisce un elenco (opens in a new tab) che può contenere solo un tipo specifico usando list[<tipo>].

1 quotes = []
2 for block in range(start_block, end_block + 1, step):

In Python un ciclo for`` (opens in a new tab) di solito itera su un elenco. L'elenco di 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 quotes

Per ogni numero di blocco, ottieni un oggetto Quote e aggiungilo all'elenco quotes. Quindi restituisci quell'elenco.

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_BLOCKS
7)
8
9pprint(quotes)
Mostra tutto

Questo è il codice principale dello script. Leggi le informazioni del gruppo, ottieni dodici quotazioni e visualizzale con pprint (opens in a new tab).

Creazione di 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-prompt
2uv run agent.py

L'output ora sarà un prompt per un LLM, simile a:

1Date queste quotazioni:
2Asset: WETH/USDC
3 2026-01-20T16:34 3016.21
4 .
5 .
6 .
7 2026-02-01T17:49 2299.10
8
9Asset: WBTC/WETH
10 2026-01-20T16:34 29.84
11 .
12 .
13 .
14 2026-02-01T17:50 33.46
15
16
17Quale valore ti aspetteresti per WETH/USDC all'ora 2026-02-02T17:56?
18
19Fornisci la tua risposta come un singolo numero arrotondato a due cifre decimali,
20senza alcun altro testo.
Mostra tutto

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 appare un prompt

Questo prompt contiene tre sezioni, che sono abbastanza comuni nei prompt LLM.

  1. Informazioni. Gli LLM hanno molte informazioni 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 generazione aumentata dal recupero (RAG) (opens in a new tab).

  2. La domanda vera e propria. Questo è ciò che vogliamo sapere.

  3. 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, timedelta

Dobbiamo fornire all'LLM l'ora per la quale vogliamo una stima. Per ottenere un'ora "n minuti/ore/giorni" nel futuro, usiamo la classe timedelta (opens in a new tab).

1# Gli indirizzi dei gruppi che stiamo leggendo
2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")
3WETHWBTC_ADDRESS = Web3.to_checksum_address("0xCBCdF9626bC03E24f779434178A73a0B4bad62eD")

Abbiamo due gruppi che dobbiamo leggere.

1@dataclass(frozen=True)
2class PoolInfo:
3 .
4 .
5 .
6 reverse: bool = False
7
8 def get_price(self, block: int) -> Decimal:
9 assert block <= w3.eth.block_number, "Il blocco è nel futuro"
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_factor
Mostra tutto

Nel gruppo WETH/USDC, vogliamo sapere quanti token0 (USDC) ci servono per acquistare un token1 (WETH). Nel gruppo WETH/WBTC, vogliamo sapere quanti token1 (WETH) ci servono per acquistare un token0 (WBTC, che è Bitcoin wrappato). Dobbiamo tenere traccia se il rapporto del gruppo 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=reverse
13 )
Mostra tutto

Per sapere se un gruppo deve essere invertito, 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 result

Questa funzione costruisce una stringa che formatta un elenco di oggetti Quote, assumendo 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 multiriga (opens in a new tab) sono scritte come """ .... """.

1Date queste quotazioni:
2{
3 functools.reduce(lambda acc, q: acc + '\n' + q,
4 map(lambda q: format_quotes(q), quotes))
5}

Qui usiamo il modello MapReduce (opens in a new tab) per generare una stringa per ogni elenco di quotazioni con format_quotes, quindi li riduciamo in una singola stringa da usare nel prompt.

1Quale valore ti aspetteresti per {asset} all'ora {expected_time}?
2
3Fornisci la tua risposta come un singolo numero arrotondato a due cifre decimali,
4senza alcun altro testo.
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_BLOCKS
15)
Mostra tutto

Esamina i due gruppi e ottieni quotazioni da entrambi.

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, inviamo un prompt a un LLM effettivo e riceviamo un valore futuro atteso. Ho scritto questo programma usando OpenAI, quindi se vuoi usare un fornitore diverso, dovrai adattarlo.

  1. Ottieni un conto OpenAI (opens in a new tab)

  2. Ricarica il conto (opens in a new tab)—l'importo minimo al momento della scrittura è di 5$

  3. Crea una chiave API (opens in a new tab)

  4. Nella riga di comando, esporta la chiave API in modo che il tuo programma possa usarla

    1export OPENAI_API_KEY=sk-<il resto della chiave va qui>
  5. Fai il checkout ed esegui l'agente

    1git checkout 04-interface-llm
    2uv run agent.py

Ecco il nuovo codice.

1from openai import OpenAI
2
3open_ai = OpenAI() # Il client legge la variabile d'ambiente OPENAI_API_KEY

Importa 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].price
3
4print ("Prezzo attuale:", wethusdc_quotes[-1].price)
5print(f"In {future_time}, prezzo previsto: {expected_price} USD")
6
7if (expected_price > current_price):
8 print(f"Compra, mi aspetto che il prezzo salga di {expected_price - current_price} USD")
9else:
10 print(f"Vendi, mi aspetto che il prezzo scenda di {current_price - expected_price} USD")
Mostra tutto

Mostra il prezzo e fornisci una raccomandazione di acquisto o vendita.

Testare le previsioni

Ora che possiamo generare previsioni, possiamo anche utilizzare dati storici per valutare se produciamo previsioni utili.

1uv run test-predictor.py

Il risultato atteso è simile a:

1Previsione per 2026-01-05T19:50: previsto 3138.93 USD, reale 3218.92 USD, errore 79.99 USD
2Previsione per 2026-01-06T19:56: previsto 3243.39 USD, reale 3221.08 USD, errore 22.31 USD
3Previsione per 2026-01-07T20:02: previsto 3223.24 USD, reale 3146.89 USD, errore 76.35 USD
4Previsione per 2026-01-08T20:11: previsto 3150.47 USD, reale 3092.04 USD, errore 58.43 USD
5.
6.
7.
8Previsione per 2026-01-31T22:33: previsto 2637.73 USD, reale 2417.77 USD, errore 219.96 USD
9Previsione per 2026-02-01T22:41: previsto 2381.70 USD, reale 2318.84 USD, errore 62.86 USD
10Previsione per 2026-02-02T22:49: previsto 2234.91 USD, reale 2349.28 USD, errore 114.37 USD
11Errore medio di previsione su 29 previsioni: 83.87103448275862068965517241 USD
12Variazione media per raccomandazione: 4.787931034482758620689655172 USD
13Varianza standard delle variazioni: 104.42 USD
14Giorni redditizi: 51.72%
15Giorni in perdita: 48.28%
Mostra tutto

La maggior parte del tester è identica all'agente, ma ecco le parti nuove o modificate.

1CYCLES_FOR_TEST = 40 # Per il backtest, quanti cicli testiamo
2
3# Ottieni molte quotazioni
4wethusdc_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_BLOCKS
18)
Mostra tutto

Guardiamo indietro di CYCLES_FOR_TEST (specificato come 40 qui) giorni.

1# Crea previsioni e confrontale con la cronologia reale
2
3total_error = Decimal(0)
4changes = []

Ci sono due tipi di errori che ci interessano. 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 $ domani, non ci importa se il risultato effettivo è 2020 $ e guadagniamo soldi extra. Ma ci importa se ha previsto 2010 $, e ha comprato ETH sulla base di 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 la cronologia completa (i valori utilizzati per la previsione e il valore del mondo reale con cui confrontarla) è disponibile. 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 le slice (opens in a new tab) per ottenere lo stesso numero di campioni 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].price
3 prediction_time_price = wethusdc_quotes[index+CYCLES_BACK-1].price

Ottieni 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 += error
3 print (f"Previsione per {prediction_time}: previsto {predicted_price} USD, reale {real_price} USD, errore {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_price
3 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, prima dobbiamo determinare la raccomandazione, poi valutare come è cambiato il prezzo effettivo e se la raccomandazione ha prodotto un guadagno (variazione positiva) o una perdita (variazione negativa).

1print (f"Errore medio di previsione su {len(wethusdc_quotes)-CYCLES_BACK} previsioni: {total_error / Decimal(len(wethusdc_quotes)-CYCLES_BACK)} USD")
2
3length_changes = Decimal(len(changes))
4mean_change = sum(changes, Decimal(0)) / length_changes
5print (f"Variazione media per raccomandazione: {mean_change} USD")
6var = sum((x - mean_change) ** 2 for x in changes) / length_changes
7print (f"Varianza standard delle variazioni: {var.sqrt().quantize(Decimal("0.01"))} USD")

Riporta i risultati.

1print (f"Giorni redditizi: {len(list(filter(lambda x: x > 0, changes)))/length_changes:.2%}")
2print (f"Giorni in perdita: {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 costosi. Il risultato è un oggetto filtro, che dobbiamo convertire in un elenco per ottenerne la lunghezza.

Invio di transazioni

Ora dobbiamo effettivamente inviare le 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.

  1. Installa Foundry (opens in a new tab)

  2. Avvia anvil (opens in a new tab)

    1anvil --fork-url https://eth.drpc.org --block-time 12

    anvil è in ascolto sull'URL predefinito per Foundry, http://localhost:8545 (opens in a new tab), quindi non è necessario specificare l'URL per il comando cast (opens in a new tab) che usiamo per manipolare la blockchain.

  3. Quando si esegue in anvil, ci sono dieci conti di prova che hanno ETH: imposta le variabili d'ambiente per il primo

    1PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    2ADDRESS=`cast wallet address $PRIVATE_KEY`
  4. Questi sono i contratti che dobbiamo usare. SwapRouter (opens in a new tab) è il contratto di Uniswap v3 che usiamo per fare trading. Potremmo fare trading direttamente attraverso il gruppo, ma questo è molto più facile.

    Le due variabili in basso sono i percorsi di Uniswap v3 necessari per scambiare tra WETH e USDC.

    1WETH_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
    2USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    3POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640
    4SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C05861564
    5WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    6USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
  5. Ognuno dei conti di prova ha 10.000 ETH. Usa il contratto WETH per wrappare 1000 ETH per ottenere 1000 WETH per il trading.

    1cast send $WETH_ADDRESS "deposit()" --value 1000ether --private-key $PRIVATE_KEY
  6. Usa SwapRouter per scambiare 500 WETH con USDC.

    1cast send $WETH_ADDRESS "approve(address,uint256)" $SWAP_ROUTER 500ether --private-key $PRIVATE_KEY
    2MAXINT=`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

    La chiamata approve crea un'autorizzazione che consente a SwapRouter di spendere alcuni dei nostri token. I contratti non possono monitorare gli eventi, quindi se trasferiamo i token direttamente al contratto SwapRouter, questo 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, quindi sa se ha avuto successo.

  7. Verifica di avere abbastanza di entrambi i token.

    1cast call $WETH_ADDRESS "balanceOf(address)" $ADDRESS | cast from-wei
    2echo `cast call $USDC_ADDRESS "balanceOf(address)" $ADDRESS | cast to-dec`/10^6 | bc

Ora che abbiamo WETH e USDC, possiamo effettivamente eseguire l'agente.

1git checkout 05-trade
2uv run agent.py

L'output sarà simile a:

1(ai-trading-agent) qbzzt@Ori-Cloudnomics:~/260215-ai-agent$ uv run agent.py
2Prezzo attuale: 1843.16
3In 2026-02-06T23:07, prezzo previsto: 1724.41 USD
4Saldi del conto prima dello scambio:
5Saldo USDC: 927301.578272
6Saldo WETH: 500
7Vendi, mi aspetto che il prezzo scenda di 118.75 USD
8Transazione di approvazione inviata: 74e367ddbb407c1aaf567d87aa5863049991b1d2aa092b6b85195d925e2bd41f
9Transazione di approvazione minata.
10Transazione di vendita inviata: fad1bcf938585c9e90364b26ac7a80eea9efd34c37e5db81e58d7655bcae28bf
11Transazione di vendita minata.
12Saldi del conto dopo lo scambio:
13Saldo USDC: 929143.797116
14Saldo WETH: 499
Mostra tutto

Per usarlo effettivamente, hai bisogno di alcune piccole modifiche.

  • Nella riga 14, cambia MAINNET_URL in un punto di accesso reale, come https://eth.drpc.org
  • Nella riga 28, cambia PRIVATE_KEY con 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 provato, 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=1

L'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. Esiste 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_ABI
5)

Le definizioni Web3 per il conto (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'autorizzazione 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. Poi 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"Transazione di approvazione inviata: {tx_hash.hex()}")
2 w3.eth.wait_for_transaction_receipt(tx_hash)
3 print("Transazione di approvazione minata.")

w3.eth.wait_for_transaction_receipt (opens in a new tab) attende che la transazione venga 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 per la vendita di 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 in entrata è 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"Transazione di acquisto inviata: {tx_hash.hex()}")
8 w3.eth.wait_for_transaction_receipt(tx_hash)
9 print("Transazione di acquisto minata.")
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"Transazione di vendita inviata: {tx_hash.hex()}")
19 w3.eth.wait_for_transaction_receipt(tx_hash)
20 print("Transazione di vendita minata.")
Mostra tutto

Le funzioni buy() e sell() sono quasi identiche. Per prima cosa approviamo un'autorizzazione 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} Saldo: {Decimal(token0_balance) / Decimal(10 ** wethusdc_pool.token0.decimals)}")
6 print(f"{wethusdc_pool.token1.symbol} Saldo: {Decimal(token1_balance) / Decimal(10 ** wethusdc_pool.token1.decimals)}")

Riporta i saldi dell'utente in entrambe le valute.

1print("Saldi del conto prima dello scambio:")
2balances()
3
4if (expected_price > current_price):
5 print(f"Compra, mi aspetto che il prezzo salga di {expected_price - current_price} USD")
6 buy(wethusdc_quotes[-1])
7else:
8 print(f"Vendi, mi aspetto che il prezzo scenda di {current_price - expected_price} USD")
9 sell()
10
11print("Saldi del conto dopo lo scambio:")
12balances()
Mostra tutto

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 una quantità fissa di WETH se si prevede che il prezzo diminuisca, indipendentemente dall'entità del calo. Probabilmente, sarebbe meglio ignorare le piccole variazioni 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 acquistarne di più. Ma se il 90% del tuo portafoglio è in WETH, potresti essere sufficientemente esposto e non c'è bisogno di acquistarne 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 vuoi comprare tu (e il prezzo sale) e cercano di vendere quando vuoi vendere tu (e il prezzo scende).

Puoi eseguire un LLM localmente, ad esempio, utilizzando LM-Studio (opens in a new tab), per evitare questo problema.

Da bot IA ad agente IA

Si può sostenere validamente che questo è 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 gruppi di Uniswap v3 e i loro ultimi valori e chiedendo quale combinazione ha 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 acquisti 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 sappia abbastanza per iniziare con gli agenti IA. Questa non è una panoramica completa dell'argomento; ci sono interi libri dedicati a questo, ma questo è sufficiente per iniziare. Buona fortuna!

Vedi qui per altri miei lavori (opens in a new tab).

Ultimo aggiornamento pagina: 10 febbraio 2026

Questo tutorial è stato utile?