Dans l'écosystème de l'intelligence artificielle en 2026, les coûts de traitement varient considérablement selon les modèles utilisés. GPT-4.1 output s'établit à 8 $/MTok, tandis que Claude Sonnet 4.5 output atteint 15 $/MTok. Les alternatives économiques comme Gemini 2.5 Flash (2,50 $/MTok) et DeepSeek V3.2 (0,42 $/MTok) democratisent l'accès aux modèles performants. Pour une entreprise traitant 10 millions de tokens par mois, le choix du modèle représente une différence de coût annuelle allant de 50 400 $ (DeepSeek) à 1 800 000 $ (Claude Sonnet 4.5). Cette réalité économique explique pourquoi les développeurs cherchent désormais à optimiser chaque aspect de leur pipeline, y compris la gestion des données de marché. Aujourd'hui, nous explorons comment Tardis.dev permet de rejouer l'historique complet des carnets d'ordres Binance avec une granularité tick-by-tick.

Qu'est-ce que Tardis.dev et pourquoi l'utiliser ?

Tardis.dev est une plateforme spécialisée dans la consommation de données de marché cryptographique en temps réel et historique. Contrairement aux API officielles des exchanges qui imposent des limitations strictes et des coûts élevés, Tardis.dev propose un accès unifié à des données de niveau commercial (exchange-grade) avec une latence minimale et une couverture exhaustive des marchés.

Pour les développeurs de stratégies de trading algorithmique, la reconstruction précise d'un order book est fondamentale. Un carnet d'ordres de niveau 2 (L2) contient l'intégralité des ordres acheteur et vendeur à chaque niveau de prix, permettant d'analyser la profondeur du marché, la liquidité disponible et les déséquilibres entre acheteurs et vendeurs.

Configuration initiale de l'environnement

Avant de commencer, installez les dépendances nécessaires pour communiquer avec l'API Tardis.dev et traiter les données de marché. Nous utiliserons le client Python officiel qui offre une interface intuitive pour la consommation des flux de données.

# Installation des dépendances
pip install tardis-client pandas numpy asyncio aiohttp

Vérification de la version installée

python -c "import tardis_client; print(f'Tardis Client v{tardis_client.__version__}')"

Créez ensuite un fichier de configuration pour centraliser vos identifiants et paramètres de connexion.

# config.py
import os

Configuration Tardis.dev API

TARDIS_API_TOKEN = os.getenv("TARDIS_API_TOKEN", "your_tardis_token_here")

Paramètres de connexion Binance

EXCHANGE = "binance" SYMBOL = "btcusdt" START_DATE = "2024-01-01T00:00:00Z" END_DATE = "2024-01-01T01:00:00Z"

Configuration HolySheep AI pour enrichissement IA

Profitez de tarifs 85% inférieurs avec HolySheep AI 👉 https://www.holysheep.ai/register

HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1" HOLYSHEEP_API_KEY = os.getenv("HOLYSHEEP_API_KEY", "your_holysheep_key")

Paramètres du carnet d'ordres

ORDER_BOOK_DEPTH = 25 # Profondeur des niveaux de prix à capturer SNAPSHOT_INTERVAL_MS = 1000 # Intervalle de capture du snapshot (1 seconde)

Implémentation du client de reconstruction d'order book

La reconstruction d'un order book historique nécessite une approche méthodique : nous devons d'abord obtenir un snapshot initial, puis appliquer séquentiellement chaque modification (delta) pour reconstruir l'état complet du carnet à chaque instant. L'implémentation suivante gère cette logique avec une précision tick-by-tick.

# order_book_replayer.py
import asyncio
from dataclasses import dataclass, field
from typing import Dict, List, Optional
from decimal import Decimal
from collections import defaultdict
import json
from datetime import datetime

@dataclass
class OrderBookLevel:
    price: Decimal
    quantity: Decimal

@dataclass
class OrderBook:
    bids: Dict[str, OrderBookLevel] = field(default_factory=dict)  # price -> level
    asks: Dict[str, OrderBookLevel] = field(default_factory=dict)
    last_update_id: int = 0
    timestamp: Optional[datetime] = None
    
    def update_bid(self, price: str, quantity: str):
        qty = Decimal(quantity)
        if qty == 0:
            self.bids.pop(price, None)
        else:
            self.bids[price] = OrderBookLevel(Decimal(price), qty)
    
    def update_ask(self, price: str, quantity: str):
        qty = Decimal(quantity)
        if qty == 0:
            self.asks.pop(price, None)
        else:
            self.asks[price] = OrderBookLevel(Decimal(price), qty)
    
    def get_spread(self) -> Optional[Decimal]:
        if not self.bids or not self.asks:
            return None
        best_bid = max(self.bids.keys(), key=lambda p: Decimal(p))
        best_ask = min(self.asks.keys(), key=lambda p: Decimal(p))
        return Decimal(best_ask) - Decimal(best_bid)
    
    def get_mid_price(self) -> Optional[Decimal]:
        if not self.bids or not self.asks:
            return None
        best_bid = max(self.bids.keys(), key=lambda p: Decimal(p))
        best_ask = min(self.asks.keys(), key=lambda p: Decimal(p))
        return (Decimal(best_bid) + Decimal(best_ask)) / 2
    
    def to_dict(self) -> dict:
        return {
            "timestamp": self.timestamp.isoformat() if self.timestamp else None,
            "last_update_id": self.last_update_id,
            "bids": [[p, str(q)] for p, q in self.bids.items()],
            "asks": [[p, str(q)] for p, q in self.asks.items()],
            "mid_price": str(self.get_mid_price()) if self.get_mid_price() else None,
            "spread": str(self.get_spread()) if self.get_spread() else None
        }

class OrderBookReplayer:
    def __init__(self, exchange: str, symbol: str, depth: int = 25):
        self.exchange = exchange
        self.symbol = symbol
        self.depth = depth
        self.order_book = OrderBook()
        self.snapshots: List[OrderBook] = []
        self.message_count = 0
        
    def process_message(self, message: dict):
        """Traite un message du flux Tardis et met à jour l'order book."""
        self.message_count += 1
        
        # Extraction du timestamp
        if "timestamp" in message:
            self.order_book.timestamp = message["timestamp"]
        
        # Gestion des snapshots (différent selon l'exchange)
        if message.get("type") == "snapshot" or "isSnapshot" in message:
            self._apply_snapshot(message)
        elif message.get("type") == "l2update" or "updates" in message:
            self._apply_delta(message)
    
    def _apply_snapshot(self, message: dict):
        """Applique un snapshot complet du carnet d'ordres."""
        self.order_book = OrderBook()
        
        # Extraction des bids et asks selon le format Binance
        bids = message.get("bids", message.get("data", {}).get("b", []))
        asks = message.get("asks", message.get("data", {}).get("a", []))
        
        for price, qty in bids[:self.depth]:
            self.order_book.update_bid(price, qty)
        
        for price, qty in asks[:self.depth]:
            self.order_book.update_ask(price, qty)
        
        if message.get("lastUpdateId"):
            self.order_book.last_update_id = message["lastUpdateId"]
    
    def _apply_delta(self, message: dict):
        """Applique les mises à jour delta au carnet existant."""
        updates = message.get("updates", 
                             message.get("data", {}).get("u", []))
        
        if isinstance(updates, list):
            for update in updates:
                if isinstance(update, dict):
                    side = update.get("side", update.get("s"))
                    price = str(update.get("price", update.get("p")))
                    qty = str(update.get("quantity", update.get("q", "0")))
                    update_id = update.get("updateId", update.get("u", 0))
                    
                    if side == "buy" or side == "b":
                        self.order_book.update_bid(price, qty)
                    elif side == "sell" or side == "s":
                        self.order_book.update_ask(price, qty)
                    
                    if update_id > self.order_book.last_update_id:
                        self.order_book.last_update_id = update_id
    
    def capture_snapshot(self):
        """Capture l'état actuel du carnet pour analyse."""
        snapshot = OrderBook(
            bids=dict(self.order_book.bids),
            asks=dict(self.order_book.asks),
            last_update_id=self.order_book.last_update_id,
            timestamp=self.order_book.timestamp
        )
        self.snapshots.append(snapshot)
        return snapshot.to_dict()
    
    def get_statistics(self) -> dict:
        """Calcule des statistiques sur les snapshots capturés."""
        if not self.snapshots:
            return {"error": "Aucun snapshot capturé"}
        
        mid_prices = [s.get_mid_price() for s in self.snapshots if s.get_mid_price()]
        
        return {
            "total_messages_processed": self.message_count,
            "snapshots_captured": len(self.snapshots),
            "price_range": {
                "min": str(min(mid_prices)) if mid_prices else None,
                "max": str(max(mid_prices)) if mid_prices else None
            },
            "avg_bid_count": sum(len(s.bids) for s in self.snapshots) / len(self.snapshots),
            "avg_ask_count": sum(len(s.asks) for s in self.snapshots) / len(self.snapshots)
        }

Connexion à l'API Tardis.dev pour la récupération historique

L'API Tardis.dev propose deux modes de consommation : le streaming temps réel et la lecture historique. Pour notre cas d'usage de backtesting, nous utiliserons le mode historique qui permet de rejouer des périodes passées avec une granularité milliseconde.

# tardis_data_loader.py
import asyncio
from tardis_client import TardisClient, Channel
from order_book_replayer import OrderBookReplayer
from datetime import datetime, timedelta
import json
import os

class TardisDataLoader:
    def __init__(self, api_token: str):
        self.client = TardisClient(api_token=api_token)
        self.replayer = None
        self.buffer = []
        
    async def replay_historical_orderbook(
        self,
        exchange: str,
        symbol: str,
        start_time: datetime,
        end_time: datetime,
        on_snapshot: callable = None
    ):
        """
        Rejoue les données historiques du carnet d'ordres.
        
        Args:
            exchange: Nom de l'exchange (ex: 'binance')
            symbol: Symbole de trading (ex: 'btcusdt')
            start_time: Début de la période
            end_time: Fin de la période
            on_snapshot: Callback appelé pour chaque snapshot capturé
        """
        self.replayer = OrderBookReplayer(exchange, symbol)
        
        print(f"📡 Connexion à Tardis.dev pour {exchange}:{symbol}")
        print(f"   Période: {start_time} -> {end_time}")
        
        # Définition des channels pour recevoir les données L2
        channels = [
            Channel(name=f"{symbol}@depth@100ms", 
                    description="Order book avec granularité 100ms")
        ]
        
        # Consommation des données via la méthode replay()
        async for local_timestamp, message in self.client.replay(
            exchange=exchange,
            channels=channels,
            from_timestamp=start_time.isoformat(),
            to_timestamp=end_time.isoformat(),
            filters=None
        ):
            try:
                # Parsing du message selon le format de l'exchange
                parsed_message = self._parse_message(message)
                
                if parsed_message:
                    self.replayer.process_message(parsed_message)
                    
                    # Capture périodique des snapshots
                    if self.replayer.message_count % 1000 == 0:
                        snapshot = self.replayer.capture_snapshot()
                        if on_snapshot:
                            await on_snapshot(snapshot)
                        print(f"   [{local_timestamp}] Messages: {self.replayer.message_count}, "
                              f"Mid: {snapshot.get('mid_price')}")
                                
            except Exception as e:
                print(f"⚠️ Erreur de parsing: {e}")
                continue
        
        # Capture du dernier état
        final_snapshot = self.replayer.capture_snapshot()
        print(f"\n✅ Replay terminé: {self.replayer.message_count} messages traités")
        
        return self.replayer.get_statistics()
    
    def _parse_message(self, message: dict) -> dict:
        """Parse les messages selon le format Binance."""
        if "type" in message:
            return message
        
        # Format raw de Binance (typique pour les flux WebSocket historiques)
        if "e" in message:
            event_type = message["e"]
            if event_type == "depthUpdate":
                return {
                    "type": "l2update",
                    "timestamp": datetime.fromtimestamp(message["E"] / 1000),
                    "updates": [
                        {"side": "buy", "price": str(b[0]), "quantity": str(b[1])}
                        for b in message["b"]
                    ] + [
                        {"side": "sell", "price": str(a[0]), "quantity": str(a[1])}
                        for a in message["a"]
                    ],
                    "lastUpdateId": message["u"]
                }
            elif event_type == "depthSnapshot":
                return {
                    "type": "snapshot",
                    "timestamp": datetime.fromtimestamp(message["E"] / 1000),
                    "bids": [[str(b[0]), str(b[1])] for b in message["bids"]],
                    "asks": [[str(a[0]), str(a[1])] for a in message["asks"]],
                    "lastUpdateId": message["lastUpdateId"]
                }
        
        return None

Exemple d'utilisation avec HolySheep AI pour analyse enrichie

async def analyze_with_holysheep(snapshot: dict): """Utilise HolySheep AI pour analyser les patterns du carnet d'ordres.""" import aiohttp url = "https://api.holysheep.ai/v1/chat/completions" headers = { "Authorization": f"Bearer {os.getenv('HOLYSHEEP_API_KEY')}", "Content-Type": "application/json" } # Analyse du déséquilibre acheteur/vendeur bids_total = sum(float(q) for _, q in snapshot.get("bids", [])[:10]) asks_total = sum(float(q) for _, q in snapshot.get("asks", [])[:10]) imbalance = (bids_total - asks_total) / (bids_total + asks_total) if (bids_total + asks_total) > 0 else 0 payload = { "model": "deepseek-v3.2", "messages": [ {"role": "system", "content": "Tu es un analyste de marché expert."}, {"role": "user", "content": f"Analyse ce carnet d'ordres BTC/USDT:\n" f"Mid price: {snapshot.get('mid_price')}\n" f"Top 10 bids: {snapshot.get('bids', [])[:5]}\n" f"Top 10 asks: {snapshot.get('asks', [])[:5]}\n" f"Déséquilibre: {imbalance:.2%}"} ], "temperature": 0.3 } async with aiohttp.ClientSession() as session: async with session.post(url, json=payload, headers=headers) as resp: if resp.status == 200: result = await resp.json() return result["choices"][0]["message"]["content"] return None async def main(): # Configuration API_TOKEN = os.getenv("TARDIS_API_TOKEN") if not API_TOKEN: print("❌ Variable d'environnement TARDIS_API_TOKEN non définie") return loader = TardisDataLoader(API_TOKEN) # Période de test : 1 heure de données BTC/USDT start = datetime(2024, 6, 15, 0, 0, 0) end = datetime(2024, 6, 15, 1, 0, 0) stats = await loader.replay_historical_orderbook( exchange="binance", symbol="btcusdt", start_time=start, end_time=end, on_snapshot=analyze_with_holysheep # Enrichissement IA optionnel ) print("\n📊 Statistiques du replay:") print(json.dumps(stats, indent=2)) if __name__ == "__main__": asyncio.run(main())

Visualisation et analyse des données reconstruites

Une fois les données rejouées, il est essentiel de les visualiser pour valider la qualité de la reconstruction et identifier les patterns de marché pertinents. Le code suivant génère des visualisations interactives du carnet d'ordres.

# visualization.py
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime
import json

def plot_order_book_depth(snapshots: list, filename: str = "orderbook_depth.png"):
    """
    Génère un graphique de profondeur du carnet d'ordres.
    
    Affiche les cumuls d'ordres par niveau de prix pour visualiser
    la liquidité disponible de chaque côté du livre.
    """
    if not snapshots:
        print("Aucun snapshot à visualiser")
        return
    
    fig, axes = plt.subplots(2, 2, figsize=(16, 12))
    
    # Graphique 1: Profondeur du marché (premier snapshot)
    ax1 = axes[0, 0]
    first = snapshots[0]
    
    bids_prices = [float(p) for p, _ in first.get("bids", [])]
    bids_qtys = [float(q) for _, q in first.get("bids", [])]
    bids_cumsum = np.cumsum(bids_qtys)
    
    asks_prices = [float(p) for p, _ in first.get("asks", [])]
    asks_qtys = [float(q) for _, q in first.get("asks", [])]
    asks_cumsum = np.cumsum(asks_qtys)
    
    ax1.fill_between(bids_prices, 0, bids_cumsum, alpha=0.5, color='green', label='Bids')
    ax1.fill_between(asks_prices, 0, asks_cumsum, alpha=0.5, color='red', label='Asks')
    ax1.set_xlabel('Prix (USDT)')
    ax1.set_ylabel('Quantité cumulative (BTC)')
    ax1.set_title('Profondeur du marché BTC/USDT')
    ax1.legend()
    ax1.grid(True, alpha=0.3)
    
    # Graphique 2: Évolution du prix médian
    ax2 = axes[0, 1]
    mid_prices = [float(s.get("mid_price", 0)) for s in snapshots if s.get("mid_price")]
    timestamps = [datetime.fromisoformat(s["timestamp"]) for s in snapshots if s.get("timestamp") and s.get("mid_price")]
    
    ax2.plot(timestamps, mid_prices, linewidth=0.8, color='blue')
    ax2.set_xlabel('Temps')
    ax2.set_ylabel('Prix médian (USDT)')
    ax2.set_title('Évolution du prix BTC/USDT')
    ax2.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
    ax2.grid(True, alpha=0.3)
    
    # Graphique 3: Spread au cours du temps
    ax3 = axes[1, 0]
    spreads = [float(s.get("spread", 0)) for s in snapshots if s.get("spread")]
    ax3.hist(spreads, bins=50, color='purple', alpha=0.7)
    ax3.set_xlabel('Spread (USDT)')
    ax3.set_ylabel('Fréquence')
    ax3.set_title('Distribution du spread')
    ax3.axvline(np.mean(spreads), color='red', linestyle='--', label=f'Moyenne: {np.mean(spreads):.2f}')
    ax3.legend()
    ax3.grid(True, alpha=0.3)
    
    # Graphique 4: Nombre d'ordres par niveau
    ax4 = axes[1, 1]
    bid_counts = [len(s.get("bids", [])) for s in snapshots]
    ask_counts = [len(s.get("asks", [])) for s in snapshots]
    
    ax4.plot(bid_counts, label='Bids', alpha=0.7)
    ax4.plot(ask_counts, label='Asks', alpha=0.7)
    ax4.set_xlabel('Snapshot')
    ax4.set_ylabel('Nombre de niveaux')
    ax4.set_title('Profondeur du carnet d\'ordres')
    ax4.legend()
    ax4.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig(filename, dpi=150, bbox_inches='tight')
    print(f"✅ Graphique sauvegardé: {filename}")
    plt.close()

def export_to_parquet(snapshots: list, filename: str = "orderbook_data.parquet"):
    """Exporte les snapshots au format Parquet pour analyse big data."""
    import pandas as pd
    
    records = []
    for i, snapshot in enumerate(snapshots):
        record = {
            "snapshot_id": i,
            "timestamp": snapshot.get("timestamp"),
            "mid_price": snapshot.get("mid_price"),
            "spread": snapshot.get("spread"),
            "bid_count": len(snapshot.get("bids", [])),
            "ask_count": len(snapshot.get("asks", [])),
            "total_bid_qty": sum(float(q) for _, q in snapshot.get("bids", [])),
            "total_ask_qty": sum(float(q) for _, q in snapshot.get("asks", []))
        }
        
        # Ajouter les top 5 niveaux
        for j in range(5):
            if j < len(snapshot.get("bids", [])):
                record[f"bid_{j}_price"] = snapshot["bids"][j][0]
                record[f"bid_{j}_qty"] = snapshot["bids"][j][1]
            if j < len(snapshot.get("asks", [])):
                record[f"ask_{j}_price"] = snapshot["asks"][j][0]
                record[f"ask_{j}_qty"] = snapshot["asks"][j][1]
        
        records.append(record)
    
    df = pd.DataFrame(records)
    df.to_parquet(filename, index=False)
    print(f"✅ Données exportées: {filename} ({len(df)} lignes)")

import numpy as np

Pour qui / pour qui ce n'est pas fait

Ce tutoriel est idéal pour :

Ce tutoriel n'est PAS recommandé pour :

Tarification et ROI

Comprendre les coûts réels des différentes solutions est crucial pour optimiser votre budget d'infrastructure de données. Voici une analyse comparative détaillée.

Solution Prix historique Latence Couverture Coût annuel (10M msg/mois)
Tardis.dev À partir de 0,50 $/million de messages ~100ms 40+ exchanges ~6 000 $
API Binance officielle Gratuit (limité à 1200 requêtes/min) ~50ms Binance uniquement 0 $ (avec limitations)
CCXT + Exchange API Gratuit (limité) Variable Multi-exchanges 0 $ (effort dev. élevé)
HolySheep AI (analyse IA) DeepSeek V3.2: 0,42 $/MTok <50ms Tous les modèles ~4 200 $ / mois

Analyse du ROI pour les entreprises :

Pourquoi choisir HolySheep

Si Tardis.dev répond à vos besoins de données de marché, HolySheep AI complète parfaitement votre stack technique pour l'analyse et le traitement intelligent de ces données.

Avantage HolySheep Détail Impact
Économie de 85%+ Taux de change ¥1=$1 (pas de majoration) GPT-4.1 à 8 $/MTok vs 30 $/MTok ailleurs
Paiement local WeChat Pay, Alipay acceptés Pas besoin de carte étrangère
Latence minimale <50ms de temps de réponse Analyse temps réel des order books
Crédits gratuits Offre de bienvenue généreuse Tests sans engagement 👉 S'inscrire ici
Multi-modèles GPT-4.1, Claude 4.5, Gemini 2.5, DeepSeek V3.2 Flexibilité selon les cas d'usage

En combinant Tardis.dev pour la collecte de données order book et HolySheep AI pour l'analyse intelligente avec des modèles performants, vous construisez un pipeline de données professionnel à une fraction du coût des solutions traditionnelles.

Erreurs courantes et solutions

Lors de l'implémentation de la reconstruction d'order book, plusieurs erreurs fréquentes peuvent survenir. Voici les cas les plus courants et leurs solutions.

Erreur Symptôme Solution
Erreur 401 Unauthorized Token API invalide ou expiré Vérifiez votre variable d'environnement TARDIS_API_TOKEN et renouvelez-le si nécessaire via le dashboard Tardis.dev
Ordre des messages incorrect Snapshots incohérents, quantities négatives Implémentez une vérification du updateId : n'appliquez un delta que si message.updateId > lastUpdateId. Sans cette validation, des messages mal ordonnés corrompent le carnet.
Dépassement de mémoire Crash avec "MemoryError" sur de longues périodes Implémentez un flush périodique : écrivez les snapshots sur disque toutes les 10 000 itérations au lieu de les garder en mémoire. Utilisez asyncpg ou polars pour le traitement par lots.
Format de message inattendu KeyError sur "bids" ou "asks" Les formats varient selon les exchanges et les périodes. Ajoutez une gestion défensive : bids = message.get("bids", message.get("data", {}).get("b", []))
Latence HolySheep élevée Temps de réponse > 200ms Vérifiez que vous utilisez bien https://api.holysheep.ai/v1 (pas api.openai.com). Batchez vos requêtes si possible pour réduire le nombre d'appels.

Code de gestion d'erreur robuste :

# Gestion défensive des erreurs de parsing
def safe_parse_message(message: dict, exchange: str) -> Optional[dict]:
    """Parse un message avec gestion complète des erreurs."""
    try:
        if not isinstance(message, dict):
            logger.warning(f"Message non-dict ignoré: {type(message)}")
            return None
        
        # Validation de base
        required_fields = ["timestamp", "symbol"]
        if not all(f in message for f in required_fields):
            logger.debug(f"Message incomplet: {message.keys()}")
            return None
        
        # Parsing spécifique par exchange
        if exchange == "binance":
            return parse_binance_message(message)
        elif exchange == "bybit":
            return parse_bybit_message(message)
        else:
            logger.warning(f"Exchange non supporté: {exchange}")
            return None
            
    except KeyError as e:
        logger.error(f"Champ manquant: {e}, message: {message}")
        return None
    except ValueError as e:
        logger.error(f"Valeur invalide: {e}, message: {message}")
        return None
    except Exception as e:
        logger.error(f"Erreur inattendue: {type(e).__name__}: {e}")
        return None

Retry pattern pour les appels HolySheep

async def call_holysheep_with_retry(payload: dict, max_retries: int = 3) -> dict: """Appelle HolySheep AI avec retry exponentiel.""" import aiohttp for attempt in range(max_retries): try: async with aiohttp.ClientSession() as session: async with session.post( "https://api.holysheep.ai/v1/chat/completions", json=payload, headers={ "Authorization": f"Bearer {HOLYSHEEP_API_KEY}", "Content-Type": "application/json" }, timeout=aiohttp.ClientTimeout(total=30) ) as resp: if resp.status == 200: return await resp.json() elif resp.status == 429: wait_time = 2 ** attempt logger.warning(f"Rate limited, retry dans {wait_time}s") await asyncio.sleep(wait_time) else: raise Exception(f"HTTP {resp.status}") except Exception as e: logger.error(f"Tentative {attempt + 1} échouée: {e}") if attempt == max_retries - 1: raise return {"error": "Max