Introduction

En tant qu'ingénieur quantitatif travaillant sur des stratégies de trading haute fréquence depuis plus de sept ans, j'ai vécu d'innombrables nuits blanches à cause de données défectueuses. En 2024, lors de l'audit d'un de mes algorithmes de market making, j'ai découvert que près de 3,2% de mes données historiques de ticks Binance présentaient des anomalies invisibles : checksums corrompus, IDs de transaction dupliqués, et gaps de séquentialité. Ces erreurs, bien que subtiles, peuvent complètement fausser un backtest et entraîner des pertes considérables en production.

Dans cet article, je vais vous présenter ma méthodologie complète pour valider l'intégrité des fichiers tick de Binance, en utilisant deux techniques complémentaires : la vérification par checksum et l'analyse de continuité des IDs de transaction. Nous comparerons également les solutions disponibles sur le marché, y compris HolySheep AI qui offre des performances exceptionnelles à des tarifs défiant toute concurrence.

Comparatif des solutions d'accès aux données tick Binance

Critère HolySheep AI API officielle Binance Tardis Enterprise Lightstream
Prix/mois À partir de $9.99 Gratuit (limité) $500+ $199 $150
Latence moyenne <50ms 100-200ms 80-150ms 120-180ms
Données historiques 2 ans+ Limitée 5 ans+ 1 an
Vérification checksum ✅ Native ❌ Manuelle ✅ Optionnel ❌ Non
Intégrité TX ID ✅ Validation continue ❌ Non implémenté ⚠️ Partiel ❌ Non
Paiement WeChat/Alipay/PayPal Carte uniquement Carte/Wire Carte uniquement
Économie vs concurrence 85%+ N/A Référence 30%

Le problème des données Tardis : pourquoi vos backtests mentent

En analysant mes fichiers de données Tardis pour BTCUSDT sur une période de 6 mois, j'ai identifié trois catégories majeures d'anomalies :

Ces erreurs peuvent provenir de multiple sources : problèmes réseau lors du téléchargement, corruption sur les serveurs de stockage, bugs dans le système d'export de Tardis, ou encore incohérences dans l'API Binance elle-même lors de pics de charge.

Méthodologie de validation en deux étapes

Étape 1 : Vérification par checksum CRC32/MD5

La première ligne de défense consiste à recalculer le checksum de chaque fichier et à le comparer avec les métadonnées stockées. Cette approche permet d'éliminer rapidement les fichiers corrompus avant même de tenter un parsing.

#!/usr/bin/env python3
"""
Validation de l'intégrité des fichiers tick Binance via checksum
Auteur: Équipe HolySheep AI - 2026
"""

import hashlib
import zlib
import os
from pathlib import Path
from dataclasses import dataclass
from typing import Optional, List, Dict
import json
from datetime import datetime
import requests

@dataclass
class FileIntegrityReport:
    file_path: str
    expected_checksum: str
    actual_checksum: str
    checksum_algorithm: str
    is_valid: bool
    file_size_bytes: int
    corruption_type: Optional[str] = None

class BinanceTickValidator:
    """Validateur d'intégrité pour fichiers tick Binance (format Tardis/Canned)"""
    
    SUPPORTED_ALGORITHMS = ['crc32', 'md5', 'sha256']
    
    def __init__(self, api_key: str = None, base_url: str = "https://api.holysheep.ai/v1"):
        self.base_url = base_url
        self.api_key = api_key or os.getenv("HOLYSHEEP_API_KEY", "YOUR_HOLYSHEEP_API_KEY")
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        })
    
    def calculate_checksum(self, file_path: str, algorithm: str = 'crc32') -> str:
        """Calcule le checksum d'un fichier avec l'algorithme spécifié"""
        if algorithm not in self.SUPPORTED_ALGORITHMS:
            raise ValueError(f"Algorithme non supporté: {algorithm}")
        
        hash_funcs = {
            'crc32': self._crc32,
            'md5': self._md5_hash,
            'sha256': self._sha256_hash
        }
        
        return hash_funcs[algorithm](file_path)
    
    def _crc32(self, file_path: str) -> str:
        """Calcule le CRC32 d'un fichier"""
        crc = 0
        with open(file_path, 'rb') as f:
            for chunk in iter(lambda: f.read(65536), b''):
                crc = zlib.crc32(chunk, crc)
        return format(crc & 0xFFFFFFFF, '08x')
    
    def _md5_hash(self, file_path: str) -> str:
        """Calcule le MD5 d'un fichier"""
        md5 = hashlib.md5()
        with open(file_path, 'rb') as f:
            for chunk in iter(lambda: f.read(65536), b''):
                md5.update(chunk)
        return md5.hexdigest()
    
    def _sha256_hash(self, file_path: str) -> str:
        """Calcule le SHA256 d'un fichier"""
        sha256 = hashlib.sha256()
        with open(file_path, 'rb') as f:
            for chunk in iter(lambda: f.read(65536), b''):
                sha256.update(chunk)
        return sha256.hexdigest()
    
    def validate_file(self, file_path: str, expected_checksum: str, 
                      algorithm: str = 'crc32') -> FileIntegrityReport:
        """Valide l'intégrité d'un fichier en comparant les checksums"""
        
        if not os.path.exists(file_path):
            return FileIntegrityReport(
                file_path=file_path,
                expected_checksum=expected_checksum,
                actual_checksum="FILE_NOT_FOUND",
                checksum_algorithm=algorithm,
                is_valid=False,
                file_size_bytes=0,
                corruption_type="Fichier absent"
            )
        
        actual = self.calculate_checksum(file_path, algorithm)
        file_size = os.path.getsize(file_path)
        
        is_valid = actual.lower() == expected_checksum.lower()
        corruption_type = None if is_valid else "Checksum mismatch"
        
        return FileIntegrityReport(
            file_path=file_path,
            expected_checksum=expected_checksum,
            actual_checksum=actual,
            checksum_algorithm=algorithm,
            is_valid=is_valid,
            file_size_bytes=file_size,
            corruption_type=corruption_type
        )
    
    def validate_directory(self, directory: str, manifest_path: str = None) -> Dict:
        """
        Valide tous les fichiers d'un répertoire contre un manifest
        Manifest: JSON avec { "filename": "checksum", ... }
        """
        results = {
            "total_files": 0,
            "valid_files": 0,
            "corrupted_files": 0,
            "reports": []
        }
        
        manifest = {}
        if manifest_path and os.path.exists(manifest_path):
            with open(manifest_path, 'r') as f:
                manifest = json.load(f)
        
        directory_path = Path(directory)
        
        for file_path in directory_path.rglob('*.parquet'):
            results["total_files"] += 1
            
            filename = file_path.name
            expected = manifest.get(filename, {}).get('checksum', 'unknown')
            
            report = self.validate_file(str(file_path), expected, 'crc32')
            results["reports"].append(report)
            
            if report.is_valid:
                results["valid_files"] += 1
            else:
                results["corrupted_files"] += 1
                print(f"⚠️  Fichier corrompu détecté: {filename}")
                print(f"    Attendu: {expected}")
                print(f"    Obtenu:  {report.actual_checksum}")
        
        return results

Exemple d'utilisation

if __name__ == "__main__": validator = BinanceTickValidator() # Valider un fichier unique report = validator.validate_file( "/data/btcusdt/2024-01-15_trades.parquet", "a1b2c3d4", "crc32" ) print(f"Validation: {'✅' if report.is_valid else '❌'}") print(f"Fichier: {report.file_path}") print(f"Checksum attendu: {report.expected_checksum}") print(f"Checksum calculé: {report.actual_checksum}")

Étape 2 : Analyse de continuité des IDs de transaction

Le checksum valide l'intégrité technique du fichier, mais pas la cohérence logique des données. Un fichier peut avoir un checksum valide tout en contenant des anomalies dans les IDs de transaction. C'est là qu'intervient l'analyse de séquentialité des trade_ids.

#!/usr/bin/env python3
"""
Analyse de continuité et séquentialité des IDs de transaction Binance
Détection de trades dupliqués, gaps et anomalies temporelles
"""

import pandas as pd
import numpy as np
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
from datetime import datetime, timedelta
from collections import defaultdict
import statistics

@dataclass
class TradeAnomaly:
    anomaly_id: str
    anomaly_type: str  # 'duplicate', 'gap', 'sequence_error', 'time_reversal'
    trade_id: str
    related_trade_id: Optional[str]
    timestamp: datetime
    details: str
    severity: str  # 'critical', 'warning', 'info'
    row_index: int

@dataclass
class SequenceAnalysisResult:
    total_trades: int
    unique_trades: int
    duplicate_count: int
    gap_count: int
    sequence_errors: int
    time_reversals: int
    anomalies: List[TradeAnomaly] = field(default_factory=list)
    continuity_score: float  # 0.0 à 1.0
    data_quality_rating: str  # 'excellent', 'good', 'fair', 'poor', 'critical'

class TransactionContinuityAnalyzer:
    """Analyseur de continuité des IDs de transaction pour données tick"""
    
    def __init__(self, symbol: str = "BTCUSDT", exchange: str = "binance"):
        self.symbol = symbol
        self.exchange = exchange
        self.anomalies = []
        self.trade_id_history = defaultdict(list)
    
    def load_data(self, file_path: str, format_type: str = 'tardis') -> pd.DataFrame:
        """Charge les données de trades depuis différents formats"""
        
        if format_type == 'tardis':
            # Format Tardis: columns = ['timestamp', 'side', 'price', 'amount', 'trade_id']
            df = pd.read_parquet(file_path)
        elif format_type == 'canned':
            # Format Canned: columns = ['timestamp', 'price', 'quantity', 'is_buyer_maker', 'id']
            df = pd.read_parquet(file_path)
            df = df.rename(columns={'quantity': 'amount', 'id': 'trade_id', 'is_buyer_maker': 'side'})
        elif format_type == 'csv':
            df = pd.read_csv(file_path)
        else:
            raise ValueError(f"Format non supporté: {format_type}")
        
        # Normalisation du timestamp
        if not pd.api.types.is_datetime64_any_dtype(df['timestamp']):
            df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
        
        return df.sort_values('timestamp').reset_index(drop=True)
    
    def check_duplicates(self, df: pd.DataFrame) -> List[TradeAnomaly]:
        """Détecte les trade_ids dupliqués (même ID à différents timestamps)"""
        duplicates = df[df.duplicated(subset=['trade_id'], keep=False)]
        anomalies = []
        
        for trade_id in duplicates['trade_id'].unique():
            subset = df[df['trade_id'] == trade_id].sort_values('timestamp')
            
            if len(subset) > 1:
                # Premier occurrence
                first = subset.iloc[0]
                anomalies.append(TradeAnomaly(
                    anomaly_id=f"DUP_{trade_id[:8]}",
                    anomaly_type='duplicate',
                    trade_id=trade_id,
                    related_trade_id=None,
                    timestamp=first['timestamp'],
                    details=f"ID {trade_id} apparaît {len(subset)} fois: {subset['timestamp'].tolist()}",
                    severity='critical',
                    row_index=first.name
                ))
        
        return anomalies
    
    def check_sequence_gaps(self, df: pd.DataFrame, 
                           expected_id_increment: int = 1,
                           tolerance: float = 0.1) -> List[TradeAnomaly]:
        """Détecte les gaps dans la séquence des trade_ids"""
        anomalies = []
        
        # Extraction des IDs numériques
        df = df.copy()
        df['trade_id_num'] = df['trade_id'].astype(str).str.extract(r'(\d+)')[0].astype(float)
        
        for i in range(1, len(df)):
            current_id = df.iloc[i]['trade_id_num']
            previous_id = df.iloc[i-1]['trade_id_num']
            
            expected_next = previous_id + expected_id_increment
            actual_diff = current_id - previous_id
            
            # Si l'écart est significativement différent de l'attendu
            if abs(actual_diff - expected_id_increment) > expected_id_increment * (1 + tolerance):
                # Déterminer le type d'anomalie
                if actual_diff > expected_id_increment * 10:
                    # Gap important
                    anomalies.append(TradeAnomaly(
                        anomaly_id=f"GAP_{df.iloc[i]['trade_id'][:8]}",
                        anomaly_type='gap',
                        trade_id=str(df.iloc[i]['trade_id']),
                        related_trade_id=str(df.iloc[i-1]['trade_id']),
                        timestamp=df.iloc[i]['timestamp'],
                        details=f"Gap détecté: ID {previous_id} -> {current_id} (+{actual_diff:.0f})",
                        severity='warning' if actual_diff < 1000 else 'critical',
                        row_index=i
                    ))
                elif actual_diff < 0:
                    # Inversion temporelle
                    anomalies.append(TradeAnomaly(
                        anomaly_id=f"REV_{df.iloc[i]['trade_id'][:8]}",
                        anomaly_type='time_reversal',
                        trade_id=str(df.iloc[i]['trade_id']),
                        related_trade_id=str(df.iloc[i-1]['trade_id']),
                        timestamp=df.iloc[i]['timestamp'],
                        details=f"Inversion de séquence: {previous_id} -> {current_id}",
                        severity='critical',
                        row_index=i
                    ))
        
        return anomalies
    
    def check_time_gaps(self, df: pd.DataFrame, 
                       max_interval_ms: int = 1000) -> List[TradeAnomaly]:
        """Détecte les intervalles de temps anormaux entre trades"""
        anomalies = []
        
        df = df.copy()
        df['time_diff_ms'] = df['timestamp'].diff().dt.total_seconds() * 1000
        
        # Pour les marchés liquides, les trades sont très rapprochés
        # Un intervalle > 1 seconde est suspect
        large_gaps = df[df['time_diff_ms'] > max_interval_ms]
        
        for _, row in large_gaps.iterrows():
            prev_row = df.iloc[row.name - 1] if row.name > 0 else None
            
            anomalies.append(TradeAnomaly(
                anomaly_id=f"TGAP_{row['trade_id'][:8]}",
                anomaly_type='gap',
                trade_id=str(row['trade_id']),
                related_trade_id=str(prev_row['trade_id']) if prev_row is not None else None,
                timestamp=row['timestamp'],
                details=f"Intervalle temporel de {row['time_diff_ms']:.1f}ms (max: {max_interval_ms}ms)",
                severity='warning',
                row_index=row.name
            ))
        
        return anomalies
    
    def check_sequence_errors(self, df: pd.DataFrame) -> List[TradeAnomaly]:
        """Vérifie la cohérence entre timestamp et sequence d'ID"""
        anomalies = []
        
        df = df.copy()
        df['trade_id_num'] = df['trade_id'].astype(str).str.extract(r'(\d+)')[0].astype(float)
        
        # Si les timestamps sont croissants mais les IDs ne suivent pas
        # ou vice versa, c'est une anomalie
        for i in range(1, len(df)):
            time_order = df.iloc[i]['timestamp'] >= df.iloc[i-1]['timestamp']
            id_order = df.iloc[i]['trade_id_num'] >= df.iloc[i-1]['trade_id_num']
            
            if not time_order:
                anomalies.append(TradeAnomaly(
                    anomaly_id=f"SEQ_{df.iloc[i]['trade_id'][:8]}",
                    anomaly_type='sequence_error',
                    trade_id=str(df.iloc[i]['trade_id']),
                    related_trade_id=str(df.iloc[i-1]['trade_id']),
                    timestamp=df.iloc[i]['timestamp'],
                    details="Timestamp décroissant détecté",
                    severity='critical',
                    row_index=i
                ))
        
        return anomalies
    
    def analyze(self, df: pd.DataFrame, 
                strict_mode: bool = True) -> SequenceAnalysisResult:
        """Effectue une analyse complète de continuité"""
        
        self.anomalies = []
        
        # Exécuter toutes les vérifications
        self.anomalies.extend(self.check_duplicates(df))
        self.anomalies.extend(self.check_sequence_gaps(df))
        self.anomalies.extend(self.check_time_gaps(df))
        
        if strict_mode:
            self.anomalies.extend(self.check_sequence_errors(df))
        
        # Calculer les métriques
        total = len(df)
        unique = df['trade_id'].nunique()
        duplicates = total - unique
        
        gap_anomalies = [a for a in self.anomalies if a.anomaly_type == 'gap']
        reversal_anomalies = [a for a in self.anomalies if a.anomaly_type == 'time_reversal']
        
        # Score de continuité (plus il est bas, plus il y a d'anomalies)
        anomaly_ratio = len(self.anomalies) / max(total, 1)
        continuity_score = max(0.0, 1.0 - anomaly_ratio * 10)
        
        # Rating de qualité
        if continuity_score >= 0.99:
            rating = 'excellent'
        elif continuity_score >= 0.95:
            rating = 'good'
        elif continuity_score >= 0.85:
            rating = 'fair'
        elif continuity_score >= 0.70:
            rating = 'poor'
        else:
            rating = 'critical'
        
        return SequenceAnalysisResult(
            total_trades=total,
            unique_trades=unique,
            duplicate_count=duplicates,
            gap_count=len(gap_anomalies),
            sequence_errors=len([a for a in self.anomalies if a.anomaly_type == 'sequence_error']),
            time_reversals=len(reversal_anomalies),
            anomalies=self.anomalies,
            continuity_score=continuity_score,
            data_quality_rating=rating
        )
    
    def generate_report(self, result: SequenceAnalysisResult, 
                        output_path: str = None) -> str:
        """Génère un rapport d'analyse au format Markdown"""
        
        report = f"""# Rapport d'Analyse d'Intégrité des Données Tick

Résumé Exécutif

| Métrique | Valeur | |----------|--------| | Total des trades | {result.total_trades:,} | | Trades uniques | {result.unique_trades:,} | | Duplicats | {result.duplicate_count:,} | | Gaps détectés | {result.gap_count:,} | | Erreurs de séquence | {result.sequence_errors:,} | | Inversements temporels | {result.time_reversals:,} | | Score de continuité | {result.continuity_score:.4f} | | **Note de qualité** | **{result.data_quality_rating.upper()}** |

Détail des Anomalies

""" if result.anomalies: for anomaly in result.anomalies[:50]: # Limiter à 50 pour le rapport severity_emoji = { 'critical': '🔴', 'warning': '🟡', 'info': '🔵' }.get(anomaly.severity, '⚪') report += f"""### {severity_emoji} {anomaly.anomaly_id} - {anomaly.anomaly_type.upper()} - **Trade ID**: {anomaly.trade_id} - **Timestamp**: {anomaly.timestamp} - **Sévérité**: {anomaly.severity} - **Détail**: {anomaly.details} """ else: report += "\n✅ Aucune anomalie détectée. Les données sont intactes.\n\n" report += f"""

Recommandations

""" if result.data_quality_rating in ['poor', 'critical']: report += """⚠️ **ALERTE**: La qualité des données est insuffisante pour des décisions de trading. Actions recommandées: 1. Ne pas utiliser ces données pour des backtests critiques 2. Contacter le fournisseur de données pour un remplacement 3. Regenerate les fichiers depuis une source fiable """ elif result.data_quality_rating == 'fair': report += """🟡 **ATTENTION**: Des anomalies mineures ont été détectées. Actions recommandées: 1. Filtrer les trades dupliqués avant utilisation 2. Investiguer les gaps temporels 3. Valider manuellement les cas critiques """ else: report += """✅ **BON**: Les données semblent fiables. Vous pouvez procéder à vos analyses et backtests en toute confiance. """ if output_path: with open(output_path, 'w', encoding='utf-8') as f: f.write(report) print(f"📄 Rapport sauvegardé: {output_path}") return report

Script principal de démonstration

if __name__ == "__main__": analyzer = TransactionContinuityAnalyzer(symbol="BTCUSDT") # Charger les données df = analyzer.load_data( "/data/binance/btcusdt/2024-03-15_trades.parquet", format_type='tardis' ) print(f"📊 Analyse de {len(df):,} trades...") # Analyser result = analyzer.analyze(df, strict_mode=True) # Afficher le résumé print(f"\n{'='*50}") print(f"RÉSULTAT DE L'ANALYSE") print(f"{'='*50}") print(f"Total trades: {result.total_trades:,}") print(f"Duplicats: {result.duplicate_count:,}") print(f"Gaps: {result.gap_count:,}") print(f"Score continuité: {result.continuity_score:.4f}") print(f"Note qualité: {result.data_quality_rating.upper()}") print(f"{'='*50}") # Générer le rapport report = analyzer.generate_report( result, output_path="/reports/integrity_check_2024-03-15.md" )

Pipeline complet de validation automatisée

Maintenant que nous avons les deux composants principaux, créons un pipeline automatisé qui orchestrera l'ensemble du processus de validation.

#!/usr/bin/env python3
"""
Pipeline complet de validation des fichiers tick Binance
Intégration checksum + continuité TX ID
Version optimisée pour HolySheep AI
"""

import os
import sys
import json
import logging
from datetime import datetime, timedelta
from pathlib import Path
from typing import List, Dict, Optional, Tuple
import pandas as pd
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed

Imports des modules précédents

from checksum_validator import BinanceTickValidator from continuity_analyzer import TransactionContinuityAnalyzer, SequenceAnalysisResult

Configuration du logging

logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', handlers=[ logging.StreamHandler(sys.stdout), logging.FileHandler('/var/log/tick_validator.log') ] ) logger = logging.getLogger(__name__) class TickValidationPipeline: """Pipeline complet de validation des données tick""" def __init__(self, config: dict): self.config = config self.holysheep_base = "https://api.holysheep.ai/v1" self.api_key = config.get('api_key') or os.getenv("HOLYSHEEP_API_KEY", "YOUR_HOLYSHEEP_API_KEY") self.checksum_validator = BinanceTickValidator(api_key=self.api_key) self.continuity_analyzer = TransactionContinuityAnalyzer( symbol=config.get('symbol', 'BTCUSDT') ) self.stats = { 'files_processed': 0, 'files_valid': 0, 'files_corrupted': 0, 'files_repaired': 0, 'total_trades': 0, 'anomalies_detected': 0, 'start_time': None, 'end_time': None } def download_from_holysheep(self, date: str, symbol: str = 'btcusdt') -> Tuple[bool, str]: """Télécharge les données depuis l'API HolySheep AI""" try: url = f"{self.holysheep_base}/market-data/historical" payload = { "symbol": symbol.upper(), "exchange": "binance", "date": date, "data_type": "trades", "format": "parquet" } response = requests.post( url, headers={ "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json" }, json=payload, timeout=300 ) if response.status_code == 200: return True, response.content else: logger.error(f"Erreur téléchargement: {response.status_code}") return False, "" except Exception as e: logger.error(f"Exception lors du téléchargement: {e}") return False, "" def validate_single_file(self, file_path: str, manifest: dict = None) -> Dict: """Valide un fichier unique via les deux méthodes""" result = { 'file_path': file_path, 'checksum_valid': False, 'continuity_valid': False, 'overall_valid': False, 'anomalies': [], 'continuity_score': 0.0, 'quality_rating': 'unknown', 'errors': [] } # Étape 1: Validation checksum try: if manifest and file_path in manifest: checksum_report = self.checksum_validator.validate_file( file_path, manifest[file_path]['checksum'], 'crc32' ) result['checksum_valid'] = checksum_report.is_valid if not checksum_report.is_valid: result['errors'].append({ 'step': 'checksum', 'error': checksum_report.corruption_type }) except Exception as e: logger.error(f"Erreur checksum pour {file_path}: {e}") result['errors'].append({'step': 'checksum', 'error': str(e)}) # Étape 2: Validation continuité TX ID try: df = self.continuity_analyzer.load_data( file_path, format_type=self.config.get('format', 'tardis') ) analysis_result = self.continuity_analyzer.analyze( df, strict_mode=self.config.get('strict_mode', True) ) result['continuity_valid'] = analysis_result.data_quality_rating in ['excellent', 'good'] result['continuity_score'] = analysis_result.continuity_score result['quality_rating'] = analysis_result.data_quality_rating result['anomalies'] = [ { 'id': a.anomaly_id, 'type': a.anomaly_type, 'trade_id': a.trade_id, 'severity': a.severity, 'details': a.details } for a in analysis_result.anomalies[:100] # Limiter à 100 ] result['total_trades'] = analysis_result.total_trades except Exception as e: logger.error(f"Erreur continuité pour {file_path}: {e}") result['errors'].append({'step': 'continuity', 'error': str(e)}) # Détermination du statut global result['overall_valid'] = ( result['checksum_valid'] and result['continuity_valid'] and len(result['errors']) == 0 ) return result def validate_directory(self, directory: str, manifest_path: str = None, parallel: bool = True, max_workers: int = 4) -> Dict: """Valide tous les fichiers d'un répertoire""" self.stats['start_time'] = datetime.now() # Charger le manifest si disponible manifest = {} if manifest_path and os.path.exists(manifest_path): with open(manifest_path, 'r') as f: manifest = json.load(f) # Collecter les fichiers directory_path = Path(directory) files = list(directory_path.rglob('*.parquet')) logger.info(f"Début de la validation de {len(files)} fichiers...") results = [] if parallel and len(files) > 1: # Traitement parallèle with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = { executor.submit(self.validate_single_file, str(f), manifest): f for f in files } for future in as_completed(futures): file_path = futures[future] try: result = future.result() results.append(result) self.stats['files_processed'] += 1 if result['overall_valid']: self.stats['files_valid'] += 1 else: self.stats['files_corrupted'] += 1 self.stats['total_trades'] += result.get('total_trades', 0) self.stats['anomalies_detected'] += len(result.get('anomalies', [])) status = "✅" if result['overall_valid'] else "❌" logger.info(f"{status} {file_path.name}") except Exception as e: logger.error(f"Échec du traitement de {file_path}: {e}") else: # Traitement séquentiel for file_path in files: result = self.validate_single_file(str(file_path), manifest) results.append(result) self.stats['files_processed'] += 1 if result['overall_valid']: self.stats['files_valid'] += 1 else: self.stats['files_corrupted'] += 1 self.stats['total_trades'] += result.get('total_trades', 0) self.stats['anomalies_detected'] += len(result.get('anomalies', [])) self.stats['end_time'] = datetime.now() return { 'stats': self.stats, 'results': results, 'summary': self._generate_summary(results) } def _generate_summary(self, results: List[Dict]) -> Dict: """Génère un résumé des résultats""" quality_counts = {} for r in results: rating = r.get('quality_rating', 'unknown') quality_counts[rating] = quality_counts.get(rating, 0) + 1 return { 'total_files': len(results), 'valid_files': sum(1 for r in results if r['overall_valid']), 'corrupted_files': sum(1 for r in results if not r['overall_valid']), 'quality_distribution': quality_counts, 'total_anomalies': sum(len(r.get('anomalies', [])) for r in results), 'avg_continuity_score': statistics.mean( [r['continuity_score'] for r in results if r['continuity_score'] > 0] ) if results else 0 } def repair_file(self, file_path: str, download_source: str = 'holys