Créez votre propre agent de trading IA sur Ethereum
Dans ce tutoriel, vous apprendrez comment créer un agent de trading IA simple. Cet agent fonctionne en suivant ces étapes :
- Lire les prix actuels et passés d'un jeton, ainsi que d'autres informations potentiellement pertinentes
- Construire une requête avec ces informations, ainsi que des informations de base pour expliquer en quoi elles pourraient être pertinentes
- Soumettre la requête et recevoir en retour un prix projeté
- Trader en fonction de la recommandation
- 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))
-
Si vous ne l'avez pas déjà, téléchargez et installez Python (opens in a new tab).
-
Clonez le dépôt GitHub.
1git clone https://github.com/qbzzt/260215-ai-agent.git -b 01-getting-started2cd 260215-ai-agent -
Installez
uv(opens in a new tab). La commande sur votre système pourrait être différente.1pipx install uv -
Téléchargez les bibliothèques.
1uv sync -
Activez l'environnement virtuel.
1source .venv/bin/activate -
Pour vérifier que Python et Web3 fonctionnent correctement, exécutez
python3et fournissez-lui ce programme. Vous pouvez le saisir à l'invite>>>; il n'est pas nécessaire de créer un fichier.1from web3 import Web32MAINNET_URL = "https://eth.drpc.org"3w3 = Web3(Web3.HTTPProvider(MAINNET_URL))4w3.eth.block_number5quit()
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-quote2uv run agent.pyVous 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 Web32from web3.contract import Contract3from decimal import Decimal, ROUND_HALF_UP4from dataclasses import dataclass5from datetime import datetime, timezone6from pprint import pprint7import time8import functools9import sysAfficher toutImportez 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 = 122MINUTE_BLOCKS = int(60 / BLOCK_TIME_SECONDS)3HOUR_BLOCKS = MINUTE_BLOCKS * 604DAY_BLOCKS = HOUR_BLOCKS * 24Un 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_BLOCKSLa 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 lisons2WETHUSDC_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]67ERC20_ABI = [8 { "name": "symbol", ... },9 { "name": "decimals", ... }10]Afficher toutCe 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: str4 symbol: str5 decimals: int6 contract: ContractC'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: str4 token0: ERC20Token5 token1: ERC20Token6 contract: Contract7 asset: str8 decimal_factor: Decimal = 1Le 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)) ** 2Pour 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
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: str4 price: Decimal5 asset: strCette 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()56 return ERC20Token(7 address=address,8 symbol=symbol,9 decimals=decimals,10 contract=token11 )Afficher toutCette 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)78 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 toutCette 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_numberSi 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.asset7 )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 quotesPour 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_BLOCKS7)89pprint(quotes)Afficher toutCeci 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-prompt2uv run agent.pyLa sortie sera désormais une invite pour un LLM, similaire à :
1Compte tenu de ces cotations :2Actif : WETH/USDC3 2026-01-20T16:34 3016.214 .5 .6 .7 2026-02-01T17:49 2299.1089Actif : WBTC/WETH10 2026-01-20T16:34 29.8411 .12 .13 .14 2026-02-01T17:50 33.46151617Quelle serait, selon vous, la valeur de WETH/USDC à la date 2026-02-02T17:56 ?1819Fournissez votre réponse sous la forme d'un nombre unique arrondi à deux décimales,20sans aucun autre texte.Afficher toutNotez 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.
-
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).
-
La question proprement dite. C'est ce que nous voulons savoir.
-
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, timedeltaNous 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 lisons2WETHUSDC_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 = False78 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_factorAfficher toutDans 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 .56 return PoolInfo(7 .8 .9 .1011 asset= f"{token1.symbol}/{token0.symbol}" if reverse else f"{token0.symbol}/{token1.symbol}",12 reverse=reverse13 )Afficher toutPour 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 resultCette 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} ?23Fournissez 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)89wethwbtc_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)Afficher toutExaminez les deux pools et obtenez des cotations de chacun.
1future_time = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat()[0:16]23print(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.
-
Créez un compte OpenAI (opens in a new tab)
-
Approvisionnez le compte (opens in a new tab)—le montant minimum au moment de la rédaction est de 5 $
-
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> -
Récupérez et exécutez l'agent
1git checkout 04-interface-llm2uv run agent.py
Voici le nouveau code.
1from openai import OpenAI23open_ai = OpenAI() # Le client lit la variable d'environnement OPENAI_API_KEYImportez 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].price34print ("Current price:", wethusdc_quotes[-1].price)5print(f"In {future_time}, expected price: {expected_price} USD")67if (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 toutAffichez 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.pyLe 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 USD2Prédiction pour 2026-01-06T19:56 : prédit 3243.39 USD, réel 3221.08 USD, erreur 22.31 USD3Prédiction pour 2026-01-07T20:02 : prédit 3223.24 USD, réel 3146.89 USD, erreur 76.35 USD4Prédiction pour 2026-01-08T20:11 : prédit 3150.47 USD, réel 3092.04 USD, erreur 58.43 USD5.6.7.8Prédiction pour 2026-01-31T22:33 : prédit 2637.73 USD, réel 2417.77 USD, erreur 219.96 USD9Prédiction pour 2026-02-01T22:41 : prédit 2381.70 USD, réel 2318.84 USD, erreur 62.86 USD10Prédiction pour 2026-02-02T22:49 : prédit 2234.91 USD, réel 2349.28 USD, erreur 114.37 USD11Erreur moyenne de prédiction sur 29 prédictions : 83.87103448275862068965517241 USD12Variation moyenne par recommandation : 4.787931034482758620689655172 USD13Variance standard des variations : 104.42 USD14Jours rentables : 51,72%15Jours de perte : 48,28%Afficher toutLa 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 testons23# Obtenir beaucoup de cotations4wethusdc_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)1112wethwbtc_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)Afficher toutNous 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éel23total_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].price3 prediction_time_price = wethusdc_quotes[index+CYCLES_BACK-1].priceObtenez 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 += error3 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_price3 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")23length_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")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.
-
Installez Foundry (opens in a new tab)
-
Démarrez
anvil(opens in a new tab)1anvil --fork-url https://eth.drpc.org --block-time 12anvilé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 commandecast(opens in a new tab) que nous utilisons pour manipuler la blockchain. -
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 premier1PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff802ADDRESS=`cast wallet address $PRIVATE_KEY` -
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=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc22USDC_ADDRESS=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483POOL_ADDRESS=0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f56404SWAP_ROUTER=0xE592427A0AEce92De3Edee1F18E0157C058615645WETH_TO_USDC=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20001F4A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB486USDC_TO_WETH=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480001F4C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 -
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 -
Utilisez
SwapRouterpour échanger 500 WETH contre des 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_KEYL'appel
approvecrée une autorisation qui permet àSwapRouterde dépenser certains de nos jetons. Les contrats ne peuvent pas surveiller les événements, donc si nous transférons des jetons directement au contratSwapRouter, il ne saurait pas qu'il a été payé. Au lieu de cela, nous autorisons le contratSwapRouterà dépenser un certain montant, puisSwapRouterle fait. Cela se fait par une fonction appelée parSwapRouter, afin qu'il sache si cela a réussi. -
Vérifiez que vous avez assez des deux jetons.
1cast call $WETH_ADDRESS "balanceOf(address)" $ADDRESS | cast from-wei2echo `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-trade2uv run agent.pyLa sortie ressemblera à ceci :
1(ai-trading-agent) qbzzt@Ori-Cloudnomics:~/260215-ai-agent$ uv run agent.py2Prix actuel : 1843.163En 2026-02-06T23:07, prix attendu : 1724.41 USD4Soldes du compte avant la transaction :5Solde USDC : 927301.5782726Solde WETH : 5007Vendre, je m'attends à ce que le prix baisse de 118.75 USD8Transaction d'approbation envoyée : 74e367ddbb407c1aaf567d87aa5863049991b1d2aa092b6b85195d925e2bd41f9Transaction d'approbation minée.10Transaction de vente envoyée : fad1bcf938585c9e90364b26ac7a80eea9efd34c37e5db81e58d7655bcae28bf11Transaction de vente minée.12Soldes du compte après la transaction :13Solde USDC : 929143.79711614Solde WETH : 499Afficher toutPour l'utiliser réellement, vous avez besoin de quelques modifications mineures.
- À la ligne 14, changez
MAINNET_URLen un point d'accès réel, commehttps://eth.drpc.org - À la ligne 28, changez
PRIVATE_KEYpar 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=1Le 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_ABI5)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.")101112def 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 toutLes 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()45 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()34if (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()1011print("Soldes du compte après la transaction :")12balances()Afficher toutCet 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
WETHsi 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