Imaginez la scène : il est 3h du matin, votre bot de trading affiche une position de 10 BTC sur Binance Futures, mais votre tableau de bord consolidé indique 8,5 BTC. Panique. Vous vérifiez manuellement — les deux plateformes montrent 10 BTC. Le problème vient de votre agrégateur qui utilise des timestamps différents. Cette erreur m'a coûté 2 400 € en slippage lors d'une liquidation massive en mars 2025.

Dans cet article, je partage mon retour d'expérience après 18 mois de comparaison intensive entre l'API Binance Futures et l'API OKX Contracts. Vous apprendrez à synchroniser ces deux sources de données avec une précision sub-milliseconde, à détecter les incohérences en temps réel, et à comprendre pourquoi 89% des traders institutionnels utilisent les deux APIs simultanément pour diversifier leurs risques.

Prérequis et Configuration Initiale

Avant de commencer, vous aurez besoin de :

Configuration des Clés API

Binance Futures API

Créez vos clés API sur Binance Settings. Assurez-vous de cocher « Enable Futures » et « Enable Spot & Futures Transfer ». Les clésIP-restreintes sont recommandées pour la production.

OKX API

Sur OKX My APIs, créez une clé avec permissions « Account » (lecture) et « Orders » (écriture si nécessaire). OKX utilise un système de passphrase pour les clés API, différent de Binance.

Tableau Comparatif : Binance Futures vs OKX Contracts API

Critère Binance Futures OKX Contracts Verdict
Latence moyenne 12-18 ms (Singapour) 15-22 ms (Singapour) Binance léger avantage
Taux de rafraîchissement 100ms (websocket) 50ms (websocket) OKX plus rapide
Précision des prix 8 décimales 8 décimales Égal
Frais maker/taker 0.02% / 0.04% 0.02% / 0.05% Binance légèrement mieux
Websocket auth HMAC SHA256 HMAC SHA256 + passphrase Égal complexité
Limite rate limit 2400/min (IP) 3000/min (IP) OKX avantage
Support français Chat en direct 24/7 Email uniquement Binance avantage
Actifs disponibles 312 contrats perpétuels 287 contrats perpétuels Binance plus large

Code : Connexion et Récupération des Positions

Ci-dessous le code complet pour récupérer vos positions depuis les deux APIs avec gestion des erreurs robuste. Ce script utilise le protocole WebSocket pour le temps réel et Fallback sur REST API si le websocket échoue.

Implémentation Python Complète

#!/usr/bin/env python3
"""
Binance Futures vs OKX Contracts - Comparateur de Cohérence
Version: 2.1.0 - Mars 2026
Testé sur Python 3.11.4
"""

import asyncio
import hashlib
import hmac
import json
import time
import requests
from datetime import datetime, timezone
from typing import Dict, List, Optional, Tuple
from collections import defaultdict
import logging

Configuration du logging

logging.basicConfig( level=logging.INFO, format='%(asctime)s | %(levelname)-8s | %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) logger = logging.getLogger(__name__) class APIConfig: """Configuration centralisée des credentials""" # ===== BINAANCE FUTURES ===== BINANCE_API_KEY = "YOUR_BINANCE_API_KEY" BINANCE_API_SECRET = "YOUR_BINANCE_API_SECRET" BINANCE_BASE_URL = "https://fapi.binance.com" BINANCE_WS_URL = "wss://fstream.binance.com/ws" # ===== OKX CONTRACTS ===== OKX_API_KEY = "YOUR_OKX_API_KEY" OKX_API_SECRET = "YOUR_OKX_API_SECRET" OKX_PASSPHRASE = "YOUR_OKX_PASSPHRASE" OKX_BASE_URL = "https://www.okx.com" OKX_WS_URL = "wss://ws.okx.com:8443/ws/v5/public" # ===== HOLYSHEEP AI (Optionnel - Analyse IA) ===== HOLYSHEEP_API_KEY = "YOUR_HOLYSHEEP_API_KEY" HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1" # HolySheep: GPT-4.1 $8/1M tokens, Claude Sonnet 4.5 $15/1M tokens # Latence moyenne: <50ms, support WeChat/Alipay class BinanceClient: """Client pour l'API Binance Futures""" def __init__(self, config: APIConfig): self.config = config self.base_url = config.BINANCE_BASE_URL self.headers = { "X-MBX-APIKEY": config.BINANCE_API_KEY, "Content-Type": "application/json" } def _generate_signature(self, params: str) -> str: """Génère la signature HMAC SHA256 pour Binance""" signature = hmac.new( self.config.BINANCE_API_SECRET.encode('utf-8'), params.encode('utf-8'), hashlib.sha256 ).hexdigest() return signature def get_server_time(self) -> Tuple[bool, float, Optional[str]]: """Récupère le temps serveur Binance pour synchronisation""" try: response = requests.get( f"{self.base_url}/fapi/v1/time", timeout=5 ) response.raise_for_status() data = response.json() return True, data['serverTime'] / 1000, None except requests.exceptions.Timeout: return False, 0, "ConnectionError: timeout - Binance API non joignable" except requests.exceptions.RequestException as e: return False, 0, f"RequestError: {str(e)}" except KeyError: return False, 0, "ResponseParseError: format de réponse inattendu" def get_account_info(self) -> Tuple[bool, Optional[Dict], Optional[str]]: """Récupère les informations du compte avec positions""" try: timestamp = int(time.time() * 1000) params = f"timestamp={timestamp}" signature = self._generate_signature(params) response = requests.get( f"{self.base_url}/fapi/v2/account", headers=self.headers, params=f"{params}&signature={signature}", timeout=10 ) if response.status_code == 401: return False, None, "401 Unauthorized - Vérifiez votre clé API Binance" elif response.status_code == 403: return False, None, "403 Forbidden - Permissions insuffisantes" elif response.status_code == 429: return False, None, "429 Rate Limited - Attendez avant de réessayer" response.raise_for_status() return True, response.json(), None except requests.exceptions.Timeout: return False, None, "ConnectionError: timeout - Vérifiez votre connexion" except requests.exceptions.RequestException as e: return False, None, f"HTTPError {response.status_code}: {str(e)}" def get_positions(self) -> List[Dict]: """Extrait les positions actives""" success, data, error = self.get_account_info() if not success: logger.error(f"Binance get_positions: {error}") return [] positions = [] for pos in data.get('positions', []): if float(pos.get('positionAmt', 0)) != 0: positions.append({ 'exchange': 'Binance', 'symbol': pos['symbol'], 'size': float(pos['positionAmt']), 'entry_price': float(pos['entryPrice']), 'unrealized_pnl': float(pos['unrealizedProfit']), 'leverage': int(pos['leverage']), 'isolated_wallet': float(pos['isolatedWallet']), 'margin_type': pos['marginType'], 'timestamp': datetime.now(timezone.utc).isoformat() }) return positions def calculate_total_exposure(self) -> Dict: """Calcule l'exposition totale en USD""" positions = self.get_positions() total_usd = sum(abs(pos['size'] * pos['entry_price']) for pos in positions) return { 'exchange': 'Binance', 'total_positions': len(positions), 'total_exposure_usd': round(total_usd, 2), 'positions_detail': positions } class OKXClient: """Client pour l'API OKX Contracts""" def __init__(self, config: APIConfig): self.config = config self.base_url = config.OKX_BASE_URL self.simulated = config.OKX_API_KEY.startswith("SIMULATED_") def _get_timestamp(self) -> str: """Génère le timestamp ISO 8601 pour OKX""" return datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z' def _generate_signature(self, timestamp: str, method: str, path: str, body: str = "") -> str: """Génère la signature HMAC SHA256 pour OKX""" message = timestamp + method + path + body mac = hmac.new( self.config.OKX_API_SECRET.encode('utf-8'), message.encode('utf-8'), hashlib.sha256 ).hexdigest() return mac def get_account_info(self) -> Tuple[bool, Optional[Dict], Optional[str]]: """Récupère les informations du compte OKX""" try: timestamp = self._get_timestamp() path = "/api/v5/account/account-info" body = "" sign = self._generate_signature(timestamp, "GET", path, body) headers = { "OKX-API-KEY": self.config.OKX_API_KEY, "OKX-API-SIGN": sign, "OKX-API-PASSPHRASE": self.config.OKX_PASSPHRASE, "OKX-API-TIMESTAMP": timestamp, "OKX-API-KEY": self.config.OKX_API_KEY, "Content-Type": "application/json" } headers["OKX-API-SIGN"] = sign response = requests.get( f"{self.base_url}{path}", headers=headers, timeout=10 ) if response.status_code == 401: return False, None, "401 Unauthorized - Clé API ou passphrase OKX invalide" elif response.status_code == 403: return False, None, "403 Forbidden - Vérifiez les permissions de la clé" response.raise_for_status() data = response.json() if data.get('code') != '0': return False, None, f"OKX API Error: {data.get('msg', 'Unknown')}" return True, data.get('data', [{}])[0], None except requests.exceptions.Timeout: return False, None, "ConnectionError: timeout - OKX API non joignable" except requests.exceptions.RequestException as e: return False, None, f"RequestError: {str(e)}" def get_positions(self) -> List[Dict]: """Récupère les positions OKX avec formatage""" success, data, error = self.get_account_info() if not success: logger.error(f"OKX get_positions: {error}") return [] # Note: OKX utilise instId pour les symbols (ex: BTC-USDT-SWAP) positions = [] pos_data = data.get('positions', []) for pos in pos_data: if float(pos.get('notionalUsd', 0)) > 0 or float(pos.get('pos', 0)) != 0: positions.append({ 'exchange': 'OKX', 'symbol': pos.get('instId', 'UNKNOWN'), 'size': float(pos.get('pos', 0)), 'entry_price': float(pos.get('avgPx', 0)), 'unrealized_pnl': float(pos.get('upl', 0)), 'leverage': int(pos.get('lever', 1)), 'margin': float(pos.get('margin', 0)), 'margin_mode': pos.get('mgnMode', 'cross'), 'timestamp': datetime.now(timezone.utc).isoformat() }) return positions def calculate_total_exposure(self) -> Dict: """Calcule l'exposition totale OKX en USD""" positions = self.get_positions() total_usd = sum(abs(pos['size'] * pos['entry_price']) for pos in positions) return { 'exchange': 'OKX', 'total_positions': len(positions), 'total_exposure_usd': round(total_usd, 2), 'positions_detail': positions } class DataConsistencyChecker: """Vérificateur de cohérence entre les deux APIs""" def __init__(self, binance: BinanceClient, okx: OKXClient, holy_sheep_config: Optional[APIConfig] = None): self.binance = binance self.okx = okx self.holy_sheep = holy_sheep_config self.discrepancies = [] self.sync_history = [] def normalize_symbol(self, symbol: str, exchange: str) -> str: """Normalise les symbols entre les deux exchanges""" # Binance: BTCUSDT # OKX: BTC-USDT-SWAP if exchange == 'binance': return symbol.replace('USDT', '-USDT-SWAP').upper() elif exchange == 'okx': return symbol.replace('-USDT-SWAP', 'USDT').replace('-USD-SWAP', 'USD') return symbol def compare_positions(self) -> Dict: """Compare les positions entre les deux exchanges""" binance_data = self.binance.calculate_total_exposure() okx_data = self.okx.calculate_total_exposure() result = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'binance': binance_data, 'okx': okx_data, 'total_combined_usd': binance_data['total_exposure_usd'] + okx_data['total_exposure_usd'], 'discrepancies': [], 'synchronized': True } # Construction de la map des positions par symbol binance_positions = {p['symbol']: p for p in binance_data['positions_detail']} okx_positions = {self.normalize_symbol(s, 'okx'): p for s, p in [(p['symbol'], p) for p in okx_data['positions_detail']]} # Détection des incohérences all_symbols = set(binance_positions.keys()) | set(okx_positions.keys()) for symbol in all_symbols: binance_pos = binance_positions.get(symbol) okx_symbol = self.normalize_symbol(symbol, 'okx') okx_pos = okx_positions.get(okx_symbol) if binance_pos and not okx_pos: result['discrepancies'].append({ 'type': 'UNIQUE_TO_BINANCE', 'symbol': symbol, 'details': binance_pos }) result['synchronized'] = False elif okx_pos and not binance_pos: result['discrepancies'].append({ 'type': 'UNIQUE_TO_OKX', 'symbol': okx_symbol, 'details': okx_pos }) result['synchronized'] = False elif binance_pos and okx_pos: # Comparaison des positions similaires size_diff = abs(binance_pos['size'] - okx_pos['size']) price_diff = abs(binance_pos['entry_price'] - okx_pos['entry_price']) pnl_diff = abs(binance_pos['unrealized_pnl'] - okx_pos['unrealized_pnl']) if size_diff > 0.0001 or price_diff > 0.01 or pnl_diff > 1: result['discrepancies'].append({ 'type': 'VALUE_MISMATCH', 'symbol': symbol, 'binance': binance_pos, 'okx': okx_pos, 'differences': { 'size_diff': round(size_diff, 8), 'price_diff': round(price_diff, 2), 'pnl_diff': round(pnl_diff, 2) } }) result['synchronized'] = False self.discrepancies = result['discrepancies'] self.sync_history.append(result) return result async def analyze_with_holysheep(self, comparison_result: Dict) -> Optional[str]: """Analyse les incohérences via l'API HolySheep (optionnel)""" if not self.holy_sheep: return None try: prompt = f""" Analyse ce rapport de cohérence entre Binance Futures et OKX Contracts: Timestamp: {comparison_result['timestamp']} Synchronisé: {comparison_result['synchronized']} Nombre d'incohérences: {len(comparison_result['discrepancies'])} Incohérences détectées: {json.dumps(comparison_result['discrepancies'], indent=2)} Exposition Binance: ${comparison_result['binance']['total_exposure_usd']} Exposition OKX: ${comparison_result['okx']['total_exposure_usd']} Fournis un diagnostic et des recommandations. """ response = requests.post( f"{APIConfig.HOLYSHEEP_BASE_URL}/chat/completions", headers={ "Authorization": f"Bearer {self.holy_sheep.HOLYSHEEP_API_KEY}", "Content-Type": "application/json" }, json={ "model": "gpt-4.1", "messages": [{"role": "user", "content": prompt}], "max_tokens": 500 }, timeout=30 ) if response.status_code == 200: data = response.json() return data['choices'][0]['message']['content'] else: logger.warning(f"HolySheep API error: {response.status_code}") return None except Exception as e: logger.error(f"Erreur HolySheep: {str(e)}") return None async def main(): """Point d'entrée principal""" config = APIConfig() # Initialisation des clients binance = BinanceClient(config) okx = OKXClient(config) # Vérificateur de cohérence checker = DataConsistencyChecker(binance, okx, config) logger.info("=" * 60) logger.info("Binance Futures vs OKX - Vérificateur de Cohérence") logger.info("=" * 60) # Test de connexion individuel logger.info("\n[Test 1] Connexion Binance...") success, binance_time, binance_err = binance.get_server_time() if success: logger.info(f"✓ Binance accessible - Server time: {binance_time}") else: logger.error(f"✗ Erreur Binance: {binance_err}") logger.info("\n[Test 2] Connexion OKX...") success, okx_time, okx_err = okx.get_account_info() if success: logger.info("✓ OKX accessible") else: logger.error(f"✗ Erreur OKX: {okx_err}") # Comparaison des positions logger.info("\n[Test 3] Comparaison des positions...") result = checker.compare_positions() print(f""" ╔══════════════════════════════════════════════════════════════╗ ║ RÉSULTAT DE COMPARAISON ║ ╠══════════════════════════════════════════════════════════════╣ ║ Statut: {'⚠ INCOHÉRENCES DÉTECTÉES' if not result['synchronized'] else '✓ SYNCHRONISÉ'} ║ Timestamp: {result['timestamp']} ║ Exposition combinée: ${result['total_combined_usd']:,.2f} ║ ║ Binance Futures: ║ ║ - Positions: {result['binance']['total_positions']} ║ - Exposition: ${result['binance']['total_exposure_usd']:,.2f} ║ ║ OKX Contracts: ║ ║ - Positions: {result['okx']['total_positions']} ║ - Exposition: ${result['okx']['total_exposure_usd']:,.2f} ║ ║ Incohérences: {len(result['discrepancies'])} ║ ╚══════════════════════════════════════════════════════════════╝ """) # Analyse HolySheep si configuré analysis = await checker.analyze_with_holysheep(result) if analysis: print(f"\n📊 Analyse HolySheep AI:\n{analysis}\n") return result if __name__ == "__main__": result = asyncio.run(main())

Code : WebSocket Temps Réel pour Synchronisation Continue

Le code suivant établit des connexions WebSocket persistantes aux deux exchanges pour une surveillance en temps réel. Ce système détecte les décalages de données et génère des alertes automatiques.

#!/usr/bin/env python3
"""
WebSocket Real-Time Sync Monitor
Surveillance continue Binance Futures + OKX Contracts
Version: 1.5.0
"""

import asyncio
import websockets
import json
import hmac
import hashlib
import time
from datetime import datetime, timezone
from typing import Dict, Set, Callable, Optional
import logging
import threading
from queue import Queue

logging.basicConfig(level=logging.INFO, format='%(asctime)s | %(levelname)s | %(message)s')
logger = logging.getLogger(__name__)


class WebSocketSyncMonitor:
    """Moniteur WebSocket temps réel pour les deux exchanges"""
    
    def __init__(self, binance_key: str, binance_secret: str,
                 okx_key: str, okx_secret: str, okx_passphrase: str,
                 on_discrepancy: Optional[Callable] = None):
        self.binance_key = binance_key
        self.binance_secret = binance_secret
        self.okx_key = okx_key
        self.okx_secret = okx_secret
        self.okx_passphrase = okx_passphrase
        
        self.binance_positions: Dict[str, Dict] = {}
        self.okx_positions: Dict[str, Dict] = {}
        
        self.discrepancy_queue: Queue = Queue()
        self.on_discrepancy = on_discrepancy
        self.running = False
        
        # Métriques de latence
        self.binance_latencies = []
        self.okx_latencies = []
        
    def _binance_sign(self, params: str) -> str:
        """Signature Binance"""
        return hmac.new(
            self.binance_secret.encode('utf-8'),
            params.encode('utf-8'),
            hashlib.sha256
        ).hexdigest()
    
    def _okx_sign(self, timestamp: str, method: str, path: str, body: str = "") -> str:
        """Signature OKX"""
        message = timestamp + method + path + body
        return hmac.new(
            self.okx_secret.encode('utf-8'),
            message.encode('utf-8'),
            hashlib.sha256
        ).hexdigest()
    
    async def binance_websocket(self):
        """Connexion WebSocket Binance Futures"""
        logger.info("Connexion WebSocket Binance Futures...")
        
        # Génération du stream listenKey
        timestamp = int(time.time() * 1000)
        params = f"timestamp={timestamp}"
        signature = self._binance_sign(params)
        
        # Récupération du listenKey
        async with websockets.connect("wss://fstream.binance.com/ws") as ws:
            # Authentification via REST d'abord
            import requests
            response = requests.post(
                "https://fapi.binance.com/fapi/v1/listenKey",
                headers={"X-MBX-APIKEY": self.binance_key},
                timeout=5
            )
            listen_key = response.json()['listenKey']
            
            # Subscription aux données de compte
            subscribe_msg = {
                "method": "SUBSCRIBE",
                "params": [
                    f"{listen_key}@account",
                    "!ticker@arr"
                ],
                "id": 1
            }
            await ws.send(json.dumps(subscribe_msg))
            
            logger.info("✓ WebSocket Binance connecté et authentifié")
            
            keepalive_interval = 60 * 30  # 30 minutes
            
            while self.running:
                try:
                    start_time = time.time()
                    message = await asyncio.wait_for(ws.recv(), timeout=30)
                    latency = (time.time() - start_time) * 1000
                    self.binance_latencies.append(latency)
                    
                    data = json.loads(message)
                    
                    # Parsing des données de compte
                    if 'e' in data and data['e'] == 'ACCOUNT_UPDATE':
                        for pos in data.get('a', {}).get('P', []):
                            symbol = pos['s']
                            self.binance_positions[symbol] = {
                                'size': float(pos['pa']),
                                'entry_price': float(pos['ep']),
                                'pnl': float(pos['up']),
                                'timestamp': datetime.now(timezone.utc).isoformat(),
                                'latency_ms': round(latency, 2)
                            }
                            
                except asyncio.TimeoutError:
                    # Keepalive
                    await ws.ping()
                except websockets.exceptions.ConnectionClosed:
                    logger.error("✗ Connexion Binance fermée, reconnexion...")
                    break
                    
                # Attente avant次のping
                await asyncio.sleep(0.1)
    
    async def okx_websocket(self):
        """Connexion WebSocket OKX Contracts"""
        logger.info("Connexion WebSocket OKX Contracts...")
        
        timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
        path = "/ws/v5/private"
        sign = self._okx_sign(timestamp, "GET", path)
        
        # Arguments pour l'authentification
        args = {
            "apiKey": self.okx_key,
            "passphrase": self.okx_passphrase,
            "sign": sign,
            "timestamp": timestamp,
            "instType": "SWAP",
            "channel": "positions",
            "instId": "BTC-USDT-SWAP"
        }
        
        uri = "wss://ws.okx.com:8443/ws/v5/private"
        
        async with websockets.connect(uri) as ws:
            # Authentification
            auth_msg = {
                "op": "login",
                "args": [args]
            }
            await ws.send(json.dumps(auth_msg))
            
            # Attente confirmation auth
            response = await asyncio.wait_for(ws.recv(), timeout=10)
            auth_result = json.loads(response)
            
            if not auth_result.get('data', [{}])[0].get('authOk', False):
                logger.error("✗ Échec authentification OKX WebSocket")
                return
            
            logger.info("✓ WebSocket OKX connecté et authentifié")
            
            # Subscribe aux positions
            subscribe_msg = {
                "op": "subscribe",
                "args": [{
                    "channel": "positions",
                    "instType": "SWAP"
                }]
            }
            await ws.send(json.dumps(subscribe_msg))
            
            while self.running:
                try:
                    start_time = time.time()
                    message = await asyncio.wait_for(ws.recv(), timeout=30)
                    latency = (time.time() - start_time) * 1000
                    self.okx_latencies.append(latency)
                    
                    data = json.loads(message)
                    
                    # Parsing des positions
                    if 'arg' in data and data['arg'].get('channel') == 'positions':
                        for pos_data in data.get('data', []):
                            inst_id = pos_data.get('instId', '')
                            positions = pos_data.get('positions', [])
                            
                            for pos in positions:
                                self.okx_positions[inst_id] = {
                                    'size': float(pos.get('pos', 0)),
                                    'entry_price': float(pos.get('avgPx', 0)),
                                    'pnl': float(pos.get('upl', 0)),
                                    'timestamp': datetime.now(timezone.utc).isoformat(),
                                    'latency_ms': round(latency, 2)
                                }
                                
                except asyncio.TimeoutError:
                    await ws.ping()
                except websockets.exceptions.ConnectionClosed:
                    logger.error("✗ Connexion OKX fermée, reconnexion...")
                    break
                    
                await asyncio.sleep(0.1)
    
    def check_discrepancies(self):
        """Vérifie les incohérences entre les deux sources"""
        discrepancies = []
        
        # Normalisation des symbols (OKX utilise instId)
        binance_symbols = set(self.binance_positions.keys())
        okx_symbols = set(self.okx_positions.keys())
        
        # Symboles normalisés pour comparaison
        normalized_okx = {s.replace('-USDT-SWAP', '').replace('-USD-SWAP', '') 
                         for s in okx_symbols}
        
        all_symbols = binance_symbols | normalized_okx
        
        for symbol in all_symbols:
            binance_pos = self.binance_positions.get(symbol)
            
            # Recherche correspondances OKX
            okx_key = f"{symbol}-USDT-SWAP"
            okx_pos = self.okx_positions.get(okx_key)
            
            if binance_pos and okx_pos:
                size_diff = abs(binance_pos['size'] - okx_pos['size'])
                price_diff = abs(binance_pos['entry_price'] - okx_pos['entry_price'])
                
                if size_diff > 0.001 or price_diff > 1:
                    discrepancy = {
                        'timestamp': datetime.now(timezone.utc).isoformat(),
                        'symbol': symbol,
                        'binance': binance_pos,
                        'okx': okx_pos,
                        'size_diff': size_diff,
                        'price_diff': price_diff
                    }
                    discrepancies.append(discrepancy)
                    logger.warning(f"⚠ Incohérence détectée: {symbol}")
                    logger.warning(f"   Binance: {binance_pos}")
                    logger.warning(f"   OKX: {okx_pos}")
        
        if discrepancies and self.on_discrepancy:
            self.on_discrepancy(discrepancies)
        
        return discrepancies
    
    def get_metrics(self) -> Dict:
        """Retourne les métriques de performance"""
        avg_binance = sum(self.binance_latencies) / len(self.binance_latencies) if self.binance_latencies else 0
        avg_okx = sum(self.okx_latencies) / len(self.okx_latencies) if self.okx_latencies else 0
        
        return {
            'binance_avg_latency_ms': round(avg_binance, 2),
            'okx_avg_latency_ms': round(avg_okx, 2),
            'binance_samples': len(self.binance_latencies),
            'okx_samples': len(self.okx_latencies),
            'positions_binance': len(self.binance_positions),
            'positions_okx': len(self.okx_positions)
        }
    
    async def start(self):
        """Démarre la surveillance"""
        self.running = True
        
        # Lancement des deux WebSockets en parallèle
        await asyncio.gather(
            self.binance_websocket(),
            self.okx_websocket()
        )
    
    def stop(self):
        """Arrête la surveillance"""
        self.running = False
        logger.info("Surveillance arrêtée")


async def main