Passer au contenu principal

Créez votre propre agent de trading IA sur Ethereum

IA
trading
agent
python
Intermédiaire
Ori Pomerantz
13 février 2026
26 minutes de lecture

Dans ce tutoriel, vous apprendrez comment créer un agent de trading IA simple. Cet agent fonctionne en suivant ces étapes :

  1. Lire les prix actuels et passés d'un jeton, ainsi que d'autres informations potentiellement pertinentes
  2. Construire une requête avec ces informations, ainsi que des informations de base pour expliquer en quoi elles pourraient être pertinentes
  3. Soumettre la requête et recevoir en retour un prix projeté
  4. Trader en fonction de la recommandation
  5. Attendre et répéter

Cet agent montre comment lire des informations, les traduire en une requête qui produit une réponse utilisable, et utiliser cette réponse. Toutes ces étapes sont nécessaires pour un agent IA. Cet agent est implémenté en Python car c'est le langage le plus courant utilisé en IA.

Pourquoi faire cela ?

Les agents de trading automatisés permettent aux développeurs de sélectionner et d'exécuter une stratégie de trading. Les agents IA permettent des stratégies de trading plus complexes et dynamiques, en utilisant potentiellement des informations et des algorithmes que le développeur n'a même pas envisagé d'utiliser.

Les outils

Ce tutoriel utilise Python (opens in a new tab), la bibliothèque Web3 (opens in a new tab), et Uniswap v3 (opens in a new tab) pour les cotations et le trading.

Pourquoi Python ?

Le langage le plus utilisé pour l'IA est Python (opens in a new tab), c'est pourquoi nous l'utilisons ici. Ne vous inquiétez pas si vous ne connaissez pas Python. Le langage est très clair, et j'expliquerai exactement ce qu'il fait.

La bibliothèque Web3 (opens in a new tab) est l'API Python pour Ethereum la plus courante. Elle est assez facile à utiliser.

Trader sur la blockchain

Il existe de nombreux échanges décentralisés (DEX) qui vous permettent de trader des jetons sur Ethereum. Cependant, ils ont tendance à avoir des taux de change similaires en raison de l'arbitrage.

Uniswap (opens in a new tab) est un DEX largement utilisé que nous pouvons utiliser à la fois pour les cotations (pour voir les valeurs relatives des jetons) et pour les transactions.

OpenAI

Pour un grand modèle de langage, j'ai choisi de commencer avec OpenAI (opens in a new tab). Pour exécuter l'application de ce tutoriel, vous devrez payer pour l'accès à l'API. Le paiement minimum de 5 $ est plus que suffisant.

Développement, étape par étape

Pour simplifier le développement, nous procédons par étapes. Chaque étape est une branche dans GitHub.

Mise en route

Voici les étapes pour commencer sous UNIX ou Linux (y compris WSL (opens in a new tab))

  1. Si vous ne l'avez pas déjà, téléchargez et installez Python (opens in a new tab).

  2. Clonez le dépôt GitHub.

    1git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started
    2cd 260215-ai-agent
  3. Installez uv (opens in a new tab). La commande sur votre système pourrait être différente.

    1pipx install uv
  4. Téléchargez les bibliothèques.

    1uv sync
  5. Activez l'environnement virtuel.

    1source .venv/bin/activate
  6. Pour vérifier que Python et Web3 fonctionnent correctement, exécutez python3 et fournissez-lui ce programme. Vous pouvez le saisir à l'invite >>> ; il n'est pas nécessaire de créer un fichier.

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

Lecture depuis la blockchain

L'étape suivante consiste à lire les données de la blockchain. Pour ce faire, vous devez passer à la branche 02-read-quote, puis utiliser uv pour exécuter le programme.

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

Vous devriez recevoir une liste d'objets Quote, chacun avec un horodatage, un prix et l'actif (actuellement toujours WETH/USDC).

Voici une explication ligne par ligne.

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
Afficher tout

Importez les bibliothèques dont nous avons besoin. Ils sont expliqués ci-dessous lorsqu'ils sont utilisés.

1print = functools.partial(print, flush=True)

Remplace le print de Python par une version qui vide toujours la sortie immédiatement. Ceci est utile dans un script de longue durée car nous ne voulons pas attendre les mises à jour de statut ou les sorties de débogage.

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

Une URL pour accéder au réseau principal. Vous pouvez en obtenir un auprès d'un nœud en tant que service ou utiliser l'un de ceux annoncés dans 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 bloc du réseau principal Ethereum est généralement créé toutes les douze secondes, il s'agit donc du nombre de blocs que nous nous attendons à voir créés sur une période donnée. Notez que ce chiffre n'est pas exact. Lorsque le proposeur de bloc est hors service, ce bloc est sauté, et le temps pour le bloc suivant est de 24 secondes. Si nous voulions obtenir le bloc exact pour un horodatage, nous utiliserions une recherche binaire (opens in a new tab). Cependant, c'est assez proche pour nos besoins. Prédire l'avenir n'est pas une science exacte.

1CYCLE_BLOCKS = DAY_BLOCKS

La taille du cycle. Nous examinons les cotations une fois par cycle et essayons d'estimer la valeur à la fin du cycle suivant.

1# L'adresse du pool que nous lisons
2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")

Les valeurs de cotation sont extraites du pool Uniswap 3 USDC/WETH à l'adresse 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 (opens in a new tab). Cette adresse est déjà au format checksum, mais il est préférable d'utiliser Web3.to_checksum_address (opens in a new tab) pour rendre le code réutilisable.

1POOL_ABI = [
2 { "name": "slot0", ... },
3 { "name": "token0", ... },
4 { "name": "token1", ... },
5]
6
7ERC20_ABI = [
8 { "name": "symbol", ... },
9 { "name": "decimals", ... }
10]
Afficher tout

Ce sont les ABI (opens in a new tab) pour les deux contrats que nous devons contacter. Pour que le code reste concis, nous n'incluons que les fonctions que nous devons appeler.

1w3 = Web3(Web3.HTTPProvider(MAINNET_URL))

Initialisez la bibliothèque Web3 (opens in a new tab) et connectez-vous à un nœud Ethereum.

1@dataclass(frozen=True)
2class ERC20Token:
3 address: str
4 symbol: str
5 decimals: int
6 contract: Contract

C'est une façon de créer une classe de données en Python. Le type de données Contract (opens in a new tab) est utilisé pour se connecter au contrat. Notez le (frozen=True). En Python, les booléens (opens in a new tab) sont définis comme True ou False, avec une majuscule. Cette classe de données est figée (frozen), ce qui signifie que les champs ne peuvent pas être modifiés.

Notez l'indentation. Contrairement aux langages dérivés de C (opens in a new tab), Python utilise l'indentation pour délimiter les blocs. L'interpréteur Python sait que la définition suivante ne fait pas partie de cette classe de données car elle ne commence pas à la même indentation que les champs de la classe de données.

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

Le type Decimal (opens in a new tab) est utilisé pour gérer avec précision les fractions décimales.

1 def get_price(self, block: int) -> Decimal:

C'est ainsi que l'on définit une fonction en Python. La définition est indentée pour montrer qu'elle fait toujours partie de PoolInfo.

Dans une fonction qui fait partie d'une classe de données, le premier paramètre est toujours self, l'instance de la classe de données qui a appelé ici. Ici, il y a un autre paramètre, le numéro de bloc.

1 assert block <= w3.eth.block_number, "Le bloc est dans le futur"

Si nous pouvions lire l'avenir, nous n'aurions pas besoin de l'IA pour le trading.

1 sqrt_price_x96 = Decimal(self.contract.functions.slot0().call(block_identifier=block)[0])

La syntaxe pour appeler une fonction sur l'EVM depuis Web3 est la suivante : <objet contrat>.functions.<nom de la fonction>().call(<paramètres>). Les paramètres peuvent être les paramètres de la fonction EVM (s'il y en a ; ici, il n'y en a pas) ou des paramètres nommés (opens in a new tab) pour modifier le comportement de la blockchain. Ici, nous en utilisons un, block_identifier, pour spécifier le numéro de bloc dans lequel nous souhaitons exécuter.

Le résultat est cette structure, sous forme de tableau (opens in a new tab). La première valeur est une fonction du taux de change entre les deux jetons.

1 raw_price = (sqrt_price_x96 / Decimal(2**96)) ** 2

Pour réduire les calculs en chaîne, Uniswap v3 ne stocke pas le facteur de change réel mais plutôt sa racine carrée. Comme l'EVM ne prend pas en charge les calculs à virgule flottante ou les fractions, au lieu de la valeur réelle, la réponse est prix&#x22C5296

1 # (jeton1 par jeton0)
2 return 1/(raw_price * self.decimal_factor)

Le prix brut que nous obtenons est le nombre de token0 que nous recevons pour chaque token1. Dans notre pool, token0 est l'USDC (un stablecoin ayant la même valeur qu'un dollar américain) et token1 est le WETH (opens in a new tab). La valeur que nous voulons vraiment est le nombre de dollars par WETH, et non l'inverse.

Le facteur décimal est le rapport entre les facteurs décimaux (opens in a new tab) pour les deux jetons.

1@dataclass(frozen=True)
2class Quote:
3 timestamp: str
4 price: Decimal
5 asset: str

Cette classe de données représente une cotation : le prix d'un actif spécifique à un moment donné. À ce stade, le champ asset n'est pas pertinent car nous utilisons un seul pool et nous n'avons donc qu'un seul actif. Cependant, nous ajouterons d'autres actifs plus tard.

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 )
Afficher tout

Cette fonction prend une adresse et renvoie des informations sur le contrat de jeton à cette adresse. Pour créer un nouveau Contract (opens in a new tab) Web3, nous fournissons l'adresse et l'ABI à 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 )
Afficher tout

Cette fonction renvoie tout ce dont nous avons besoin à propos d'un pool spécifique (opens in a new tab). La syntaxe f"<string>" est une chaîne de caractères formatée (opens in a new tab).

1def get_quote(pool: PoolInfo, block_number: int = None) -> Quote:

Obtenez un objet Quote. La valeur par défaut pour block_number est None (aucune valeur).

1 if block_number is None:
2 block_number = w3.eth.block_number

Si un numéro de bloc n'a pas été spécifié, utilisez w3.eth.block_number, qui est le dernier numéro de bloc. Voici la syntaxe d'une instruction if (opens in a new tab).

On pourrait penser qu'il aurait été préférable de définir la valeur par défaut à w3.eth.block_number, mais cela ne fonctionne pas bien car ce serait le numéro de bloc au moment où la fonction est définie. Dans un agent fonctionnant en continu, cela poserait problème.

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 )

Utilisez la bibliothèque datetime (opens in a new tab) pour la formater dans un format lisible par les humains et les grands modèles de langage (LLM). Utilisez Decimal.quantize (opens in a new tab) pour arrondir la valeur à deux décimales.

1def get_quotes(pool: PoolInfo, start_block: int, end_block: int, step: int) -> list[Quote]:

En Python, vous définissez une liste (opens in a new tab) qui ne peut contenir qu'un type spécifique en utilisant list[<type>].

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

En Python, une boucle for (opens in a new tab) itère généralement sur une liste. La liste des numéros de bloc dans lesquels trouver des cotations provient de range (opens in a new tab).

1 quote = get_quote(pool, block)
2 quotes.append(quote)
3 return quotes

Pour chaque numéro de bloc, obtenez un objet Quote et ajoutez-le à la liste quotes. Retournez ensuite cette liste.

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)
Afficher tout

Ceci est le code principal du script. Lisez les informations du pool, obtenez douze cotations et pprint (opens in a new tab)-les.

Créer une invite

Ensuite, nous devons convertir cette liste de cotations en une invite pour un LLM et obtenir une valeur future attendue.

1git checkout 03-create-prompt
2uv run agent.py

La sortie sera désormais une invite pour un LLM, similaire à :

1Compte tenu de ces cotations :
2Actif : WETH/USDC
3 2026-01-20T16:34 3016.21
4 .
5 .
6 .
7 2026-02-01T17:49 2299.10
8
9Actif : WBTC/WETH
10 2026-01-20T16:34 29.84
11 .
12 .
13 .
14 2026-02-01T17:50 33.46
15
16
17Quelle serait, selon vous, la valeur de WETH/USDC à la date 2026-02-02T17:56 ?
18
19Fournissez votre réponse sous la forme d'un nombre unique arrondi à deux décimales,
20sans aucun autre texte.
Afficher tout

Notez qu'il y a ici des cotations pour deux actifs, WETH/USDC et WBTC/WETH. L'ajout de cotations d'un autre actif pourrait améliorer la précision de la prédiction.

À quoi ressemble une invite

Cette invite contient trois sections, qui sont assez courantes dans les invites de LLM.

  1. Informations. Les LLM disposent de beaucoup d'informations provenant de leur entraînement, mais ils n'ont généralement pas les plus récentes. C'est la raison pour laquelle nous devons récupérer les dernières cotations ici. L'ajout d'informations à une invite est appelé génération augmentée par récupération (RAG) (opens in a new tab).

  2. La question proprement dite. C'est ce que nous voulons savoir.

  3. Instructions de formatage de la sortie. Normalement, un LLM nous donnera une estimation avec une explication sur la façon dont il y est parvenu. C'est mieux pour les humains, mais un programme informatique n'a besoin que du résultat final.

Explication du code

Voici le nouveau code.

1from datetime import datetime, timezone, timedelta

Nous devons fournir au LLM le moment pour lequel nous voulons une estimation. Pour obtenir un temps « n minutes/heures/jours » dans le futur, nous utilisons la classe timedelta (opens in a new tab).

1# Les adresses des pools que nous lisons
2WETHUSDC_ADDRESS = Web3.to_checksum_address("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")
3WETHWBTC_ADDRESS = Web3.to_checksum_address("0xCBCdF9626bC03E24f779434178A73a0B4bad62eD")

Nous avons deux pools à lire.

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, "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_factor
Afficher tout

Dans le pool WETH/USDC, nous voulons savoir combien de token0 (USDC) nous avons besoin pour acheter un token1 (WETH). Dans le pool WETH/WBTC, nous voulons savoir combien de token1 (WETH) nous avons besoin pour acheter un token0 (WBTC, qui est du Bitcoin encapsulé). Nous devons savoir si le ratio du pool doit être inversé.

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 )
Afficher tout

Pour savoir si un pool doit être inversé, nous devons obtenir cela en entrée de read_pool. De plus, le symbole de l'actif doit être correctement configuré.

La syntaxe <a> if <b> else <c> est l'équivalent Python de l'opérateur conditionnel ternaire (opens in a new tab), qui dans un langage dérivé de C serait <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

Cette fonction construit une chaîne de caractères qui formate une liste d'objets Quote, en supposant qu'ils s'appliquent tous au même actif.

1def make_prompt(quotes: list[list[Quote]], expected_time: str, asset: str) -> str:
2 return f"""

En Python, les littéraux de chaînes de caractères sur plusieurs lignes (opens in a new tab) s'écrivent comme """ .... """.

1Compte tenu de ces cotations :
2{
3 functools.reduce(lambda acc, q: acc + '\n' + q,
4 map(lambda q: format_quotes(q), quotes))
5}

Ici, nous utilisons le modèle MapReduce (opens in a new tab) pour générer une chaîne de caractères pour chaque liste de cotations avec format_quotes, puis nous les réduisons en une seule chaîne de caractères à utiliser dans l'invite.

1Quelle serait la valeur attendue pour {asset} au moment {expected_time} ?
2
3Fournissez votre réponse sous la forme d'un nombre unique arrondi à deux décimales,
4sans aucun autre texte.
5 """

Le reste de l'invite est conforme aux attentes.

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)
Afficher tout

Examinez les deux pools et obtenez des cotations de chacun.

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

Déterminez le moment futur pour lequel nous voulons l'estimation, et créez l'invite.

Interaction avec un LLM

Ensuite, nous interrogeons un LLM réel et recevons une valeur future attendue. J'ai écrit ce programme en utilisant OpenAI, donc si vous voulez utiliser un autre fournisseur, vous devrez l'ajuster.

  1. Créez un compte OpenAI (opens in a new tab)

  2. Approvisionnez le compte (opens in a new tab)—le montant minimum au moment de la rédaction est de 5 $

  3. Créez une clé d'API (opens in a new tab)

  4. Dans la ligne de commande, exportez la clé d'API pour que votre programme puisse l'utiliser

    1export OPENAI_API_KEY=sk-<le reste de la clé va ici>
  5. Récupérez et exécutez l'agent

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

Voici le nouveau code.

1from openai import OpenAI
2
3open_ai = OpenAI() # Le client lit la variable d'environnement OPENAI_API_KEY

Importez et instanciez l'API 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)

Appelez l'API OpenAI (open_ai.chat.completions.create) pour créer la réponse.

1expected_price = Decimal(response.choices[0].message.content.strip())
2current_price = wethusdc_quotes[-1].price
3
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")
Afficher tout

Affichez le prix et fournissez une recommandation d'achat ou de vente.

Tester les prédictions

Maintenant que nous pouvons générer des prédictions, nous pouvons également utiliser des données historiques pour évaluer si nous produisons des prédictions utiles.

1uv run test-predictor.py

Le résultat attendu est similaire à :

1Prédiction pour 2026-01-05T19:50 : prédit 3138.93 USD, réel 3218.92 USD, erreur 79.99 USD
2Prédiction pour 2026-01-06T19:56 : prédit 3243.39 USD, réel 3221.08 USD, erreur 22.31 USD
3Prédiction pour 2026-01-07T20:02 : prédit 3223.24 USD, réel 3146.89 USD, erreur 76.35 USD
4Prédiction pour 2026-01-08T20:11 : prédit 3150.47 USD, réel 3092.04 USD, erreur 58.43 USD
5.
6.
7.
8Prédiction pour 2026-01-31T22:33 : prédit 2637.73 USD, réel 2417.77 USD, erreur 219.96 USD
9Prédiction pour 2026-02-01T22:41 : prédit 2381.70 USD, réel 2318.84 USD, erreur 62.86 USD
10Prédiction pour 2026-02-02T22:49 : prédit 2234.91 USD, réel 2349.28 USD, erreur 114.37 USD
11Erreur moyenne de prédiction sur 29 prédictions : 83.87103448275862068965517241 USD
12Variation moyenne par recommandation : 4.787931034482758620689655172 USD
13Variance standard des variations : 104.42 USD
14Jours rentables : 51,72%
15Jours de perte : 48,28%
Afficher tout

La majeure partie du testeur est identique à l'agent, mais voici les parties qui sont nouvelles ou modifiées.

1CYCLES_FOR_TEST = 40 # Pour le backtest, combien de cycles nous testons
2
3# Obtenir beaucoup de cotations
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)
Afficher tout

Nous regardons en arrière sur CYCLES_FOR_TEST (spécifié à 40 ici) jours.

1# Créer des prédictions et les vérifier par rapport à l'historique réel
2
3total_error = Decimal(0)
4changes = []

Il y a deux types d'erreurs qui nous intéressent. La première, total_error, est simplement la somme des erreurs commises par le prédicteur.

Pour comprendre la seconde, changes, nous devons nous rappeler le but de l'agent. Il ne s'agit pas de prédire le ratio WETH/USDC (prix de l'ETH). Il s'agit d'émettre des recommandations de vente et d'achat. Si le prix est actuellement de 2000 $ et qu'il prédit 2010 $ demain, peu nous importe si le résultat réel est de 2020 $ et que nous gagnons de l'argent supplémentaire. Mais nous nous soucions s'il a prédit 2010 $, et a acheté de l'ETH sur la base de cette recommandation, et que le prix chute à 1990 $.

1for index in range(0,len(wethusdc_quotes)-CYCLES_BACK):

Nous ne pouvons examiner que les cas où l'historique complet (les valeurs utilisées pour la prédiction et la valeur réelle pour la comparer) est disponible. Cela signifie que le cas le plus récent doit être celui qui a commencé il y a CYCLES_BACK.

1 wethusdc_slice = wethusdc_quotes[index:index+CYCLES_BACK]
2 wethwbtc_slice = wethwbtc_quotes[index:index+CYCLES_BACK]

Utilisez des tranches (opens in a new tab) pour obtenir le même nombre d'échantillons que celui utilisé par l'agent. Le code entre ici et le segment suivant est le même code d'obtention de prédiction que nous avons dans l'agent.

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

Obtenez le prix prédit, le prix réel et le prix au moment de la prédiction. Nous avons besoin du prix au moment de la prédiction pour déterminer si la recommandation était d'acheter ou de vendre.

1 error = abs(predicted_price - real_price)
2 total_error += error
3 print (f"Prediction for {prediction_time}: predicted {predicted_price} USD, real {real_price} USD, error {error} USD")

Calculez l'erreur et ajoutez-la au total.

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)

Pour les changements, nous voulons l'impact monétaire de l'achat ou de la vente d'un ETH. Donc, d'abord, nous devons déterminer la recommandation, puis évaluer comment le prix réel a changé, et si la recommandation a fait gagner de l'argent (changement positif) ou a coûté de l'argent (changement négatif).

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_changes
5print (f"Mean change per recommendation: {mean_change} USD")
6var = sum((x - mean_change) ** 2 for x in changes) / length_changes
7print (f"Standard variance of changes: {var.sqrt().quantize(Decimal("0.01"))} USD")

Rapportez les résultats.

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%}")

Utilisez filter (opens in a new tab) pour compter le nombre de jours rentables et le nombre de jours coûteux. Le résultat est un objet de filtre, que nous devons convertir en liste pour en obtenir la longueur.

Soumettre des transactions

Maintenant, nous devons réellement soumettre des transactions. Cependant, je ne veux pas dépenser de l'argent réel à ce stade, avant que le système ne soit éprouvé. Au lieu de cela, nous allons créer une fourche locale du réseau principal, et « trader » sur ce réseau.

Voici les étapes pour créer une fourche locale et permettre le trading.

  1. Installez Foundry (opens in a new tab)

  2. Démarrez anvil (opens in a new tab)

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

    anvil écoute sur l'URL par défaut de Foundry, http://localhost:8545 (opens in a new tab), nous n'avons donc pas besoin de spécifier l'URL pour la commande cast (opens in a new tab) que nous utilisons pour manipuler la blockchain.

  3. Lors de l'exécution dans anvil, il y a dix comptes de test qui ont de l'ETH—définissez les variables d'environnement pour le premier

    1PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    2ADDRESS=`cast wallet address $PRIVATE_KEY`
  4. Ce sont les contrats que nous devons utiliser. SwapRouter (opens in a new tab) est le contrat Uniswap v3 que nous utilisons pour réellement trader. Nous pourrions trader directement via le pool, mais c'est beaucoup plus facile.

    Les deux variables du bas sont les chemins Uniswap v3 requis pour échanger entre le WETH et l'USDC.

    1WETH_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
    2USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    3POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640
    4SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C05861564
    5WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    6USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
  5. Chacun des comptes de test dispose de 10 000 ETH. Utilisez le contrat WETH pour envelopper 1000 ETH afin d'obtenir 1000 WETH pour le trading.

    1cast send $WETH_ADDRESS "deposit()" --value 1000ether --private-key $PRIVATE_KEY
  6. Utilisez SwapRouter pour échanger 500 WETH contre des 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

    L'appel approve crée une autorisation qui permet à SwapRouter de dépenser certains de nos jetons. Les contrats ne peuvent pas surveiller les événements, donc si nous transférons des jetons directement au contrat SwapRouter, il ne saurait pas qu'il a été payé. Au lieu de cela, nous autorisons le contrat SwapRouter à dépenser un certain montant, puis SwapRouter le fait. Cela se fait par une fonction appelée par SwapRouter, afin qu'il sache si cela a réussi.

  7. Vérifiez que vous avez assez des deux jetons.

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

Maintenant que nous avons des WETH et des USDC, nous pouvons réellement exécuter l'agent.

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

La sortie ressemblera à ceci :

1(ai-trading-agent) qbzzt@Ori-Cloudnomics:~/260215-ai-agent$ uv run agent.py
2Prix actuel : 1843.16
3En 2026-02-06T23:07, prix attendu : 1724.41 USD
4Soldes du compte avant la transaction :
5Solde USDC : 927301.578272
6Solde WETH : 500
7Vendre, je m'attends à ce que le prix baisse de 118.75 USD
8Transaction d'approbation envoyée : 74e367ddbb407c1aaf567d87aa5863049991b1d2aa092b6b85195d925e2bd41f
9Transaction d'approbation minée.
10Transaction de vente envoyée : fad1bcf938585c9e90364b26ac7a80eea9efd34c37e5db81e58d7655bcae28bf
11Transaction de vente minée.
12Soldes du compte après la transaction :
13Solde USDC : 929143.797116
14Solde WETH : 499
Afficher tout

Pour l'utiliser réellement, vous avez besoin de quelques modifications mineures.

  • À la ligne 14, changez MAINNET_URL en un point d'accès réel, comme https://eth.drpc.org
  • À la ligne 28, changez PRIVATE_KEY par votre propre clé privée
  • À moins que vous ne soyez très riche et que vous puissiez acheter ou vendre 1 ETH chaque jour pour un agent non éprouvé, vous voudrez peut-être modifier 29 pour diminuer WETH_TRADE_AMOUNT

Explication du code

Voici le nouveau code.

1SWAP_ROUTER_ADDRESS=Web3.to_checksum_address("0xE592427A0AEce92De3Edee1F18E0157C05861564")
2WETH_TO_USDC=bytes.fromhex("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
3USDC_TO_WETH=bytes.fromhex("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
4PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"

Les mêmes variables que nous avons utilisées à l'étape 4.

1WETH_TRADE_AMOUNT=1

Le montant à trader.

1ERC20_ABI = [
2 { "name": "symbol", ... },
3 { "name": "decimals", ... },
4 { "name": "balanceOf", ...},
5 { "name": "approve", ...}
6]

Pour réellement trader, nous avons besoin de la fonction approve. Nous voulons également afficher les soldes avant et après, nous avons donc également besoin de balanceOf.

1SWAP_ROUTER_ABI = [
2 { "name": "exactInput", ...},
3]

Dans l'ABI SwapRouter, nous n'avons besoin que de exactInput. Il existe une fonction connexe, exactOutput, que nous pourrions utiliser pour acheter exactement un WETH, mais par souci de simplicité, nous utilisons simplement exactInput dans les deux cas.

1account = w3.eth.account.from_key(PRIVATE_KEY)
2swap_router = w3.eth.contract(
3 address=SWAP_ROUTER_ADDRESS,
4 abi=SWAP_ROUTER_ABI
5)

Les définitions Web3 pour le compte (opens in a new tab) et le contrat 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 }

Les paramètres de la transaction. Nous avons besoin d'une fonction ici car le nonce (opens in a new tab) doit changer à chaque fois.

1def approve_token(contract: Contract, amount: int):

Approuvez une autorisation de jeton pour 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)

Voici comment nous envoyons une transaction dans Web3. D'abord, nous utilisons l'objet Contract (opens in a new tab) pour construire la transaction. Ensuite, nous utilisons web3.eth.account.sign_transaction (opens in a new tab) pour signer la transaction, en utilisant PRIVATE_KEY. Enfin, nous utilisons w3.eth.send_raw_transaction (opens in a new tab) pour envoyer la transaction.

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) attend que la transaction soit minée. Il renvoie le reçu si nécessaire.

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}

Ce sont les paramètres lors de la vente de 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 }

Contrairement à SELL_PARAMS, les paramètres d'achat peuvent changer. Le montant d'entrée est le coût de 1 WETH, tel qu'il est disponible dans la cotation.

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.")
Afficher tout

Les fonctions buy() et sell() sont presque identiques. D'abord, nous approuvons une autorisation suffisante pour SwapRouter, puis nous l'appelons avec le bon chemin et le bon montant.

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)}")

Rapportez les soldes des utilisateurs dans les deux devises.

1print("Soldes du compte avant la transaction :")
2balances()
3
4if (expected_price > current_price):
5 print(f"Acheter, je m'attends à ce que le prix augmente de {expected_price - current_price} USD")
6 buy(wethusdc_quotes[-1])
7else:
8 print(f"Vendre, je m'attends à ce que le prix baisse de {current_price - expected_price} USD")
9 sell()
10
11print("Soldes du compte après la transaction :")
12balances()
Afficher tout

Cet agent ne fonctionne actuellement qu'une seule fois. Cependant, vous pouvez le modifier pour qu'il fonctionne en continu, soit en l'exécutant à partir de crontab (opens in a new tab), soit en encapsulant les lignes 368-400 dans une boucle et en utilisant time.sleep (opens in a new tab) pour attendre qu'il soit temps de passer au cycle suivant.

Améliorations possibles

Ce n'est pas une version de production complète ; c'est simplement un exemple pour enseigner les bases. Voici quelques idées d'améliorations.

Trading plus intelligent

Il y a deux faits importants que l'agent ignore lorsqu'il décide quoi faire.

  • L'ampleur du changement anticipé. L'agent vend un montant fixe de WETH si le prix est censé baisser, quelle que soit l'ampleur de la baisse. On pourrait soutenir qu'il serait préférable d'ignorer les changements mineurs et de vendre en fonction de l'ampleur de la baisse attendue du prix.
  • Le portefeuille actuel. Si 10 % de votre portefeuille est en WETH et que vous pensez que le prix va augmenter, il est probablement judicieux d'en acheter davantage. Mais si 90 % de votre portefeuille est en WETH, vous êtes peut-être suffisamment exposé, et il n'est pas nécessaire d'en acheter davantage. L'inverse est vrai si vous vous attendez à ce que le prix baisse.

Et si vous voulez garder votre stratégie de trading secrète ?

Les fournisseurs d'IA peuvent voir les requêtes que vous envoyez à leurs LLM, ce qui pourrait exposer le système de trading de génie que vous avez développé avec votre agent. Un système de trading que trop de gens utilisent est sans valeur car trop de gens essaient d'acheter quand vous voulez acheter (et le prix monte) et essaient de vendre quand vous voulez vendre (et le prix baisse).

Vous pouvez exécuter un LLM localement, par exemple en utilisant LM-Studio (opens in a new tab), pour éviter ce problème.

Du bot IA à l'agent IA

On peut affirmer qu'il s'agit d'un bot IA, et non d'un agent IA. Il met en œuvre une stratégie relativement simple qui repose sur des informations prédéfinies. Nous pouvons permettre l'auto-amélioration, par exemple, en fournissant une liste de pools Uniswap v3 et leurs dernières valeurs et en demandant quelle combinaison a la meilleure valeur prédictive.

Protection contre le glissement de prix

Actuellement, il n'y a pas de protection contre le glissement de prix (opens in a new tab). Si la cotation actuelle est de 2 000 $ et que le prix attendu est de 2 100 $, l'agent achètera. Cependant, si avant que l'agent n'achète, le coût monte à 2 200 $, il n'est plus logique d'acheter.

Pour mettre en œuvre une protection contre le glissement de prix, spécifiez une valeur amountOutMinimum aux lignes 325 et 334 de agent.py (opens in a new tab).

Conclusion

Espérons que vous en savez maintenant assez pour commencer avec les agents IA. Ce n'est pas un aperçu complet du sujet ; des livres entiers y sont consacrés, mais c'est suffisant pour vous lancer. Bonne chance !

Voir ici pour plus de mon travail (opens in a new tab).

Dernière mise à jour de la page : 10 février 2026

Ce tutoriel vous a été utile ?