//! # Data Validator //! //! Orchestrates multiple validation rules and generates comprehensive reports. use std::fmt::Write as _; use super::rules::{Severity, ValidationError, ValidationRule}; use crate::Indicators; use crate::OHLCVBar; use anyhow::Result; use std::sync::atomic::{AtomicUsize, Ordering}; /// Validation result #[derive(Debug, Clone)] pub struct ValidationResult { /// Validation errors (critical issues) pub errors: Vec, /// Validation warnings (potential issues) pub warnings: Vec, /// Total bars validated pub total_bars: usize, /// Validation passed (no errors) valid: bool, } impl ValidationResult { /// Create new validation result pub fn new( errors: Vec, warnings: Vec, total_bars: usize, ) -> Self { let valid = errors.is_empty(); Self { errors, warnings, total_bars, valid, } } /// Check if validation passed (no errors) pub const fn is_valid(&self) -> bool { self.valid } /// Get error count pub fn error_count(&self) -> usize { self.errors.len() } /// Get warning count pub fn warning_count(&self) -> usize { self.warnings.len() } /// Get summary of all errors pub fn error_summary(&self) -> String { let mut summary = String::new(); for error in &self.errors { _ = writeln!(summary, "{}: {}", error.category, error.message); } summary } /// Generate formatted validation report pub fn generate_report(&self) -> String { let mut report = String::new(); report.push_str("\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\n"); report.push_str(" DATA VALIDATION REPORT\n"); report.push_str("\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\n\n"); // Overall status if self.is_valid() { report.push_str("\u{2705} Status: PASS\n"); } else { report.push_str("\u{274c} Status: FAIL\n"); } _ = writeln!(report, "\u{1f4ca} Total bars validated: {}", self.total_bars); _ = writeln!(report, "\u{1f534} Errors: {}", self.error_count()); _ = writeln!(report, "\u{1f7e1} Warnings: {}\n", self.warning_count()); // Errors section if !self.errors.is_empty() { report.push_str("\u{1f534} ERRORS:\n"); report.push_str("\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n"); // Group errors by category let mut categories = std::collections::HashMap::new(); for error in &self.errors { categories .entry(error.category.clone()) .or_insert_with(Vec::new) .push(error); } for (category, errors) in categories { _ = writeln!(report, "\n {} ({} errors):", category, errors.len()); for error in errors.iter().take(5) { // Show first 5 errors per category if let Some(idx) = error.bar_index { _ = writeln!(report, " [Bar {}] {}", idx, error.message); } else { _ = writeln!(report, " {}", error.message); } } if errors.len() > 5 { _ = writeln!(report, " ... and {} more", errors.len() - 5); } } report.push('\n'); } // Warnings section if !self.warnings.is_empty() { report.push_str("\u{1f7e1} WARNINGS:\n"); report.push_str("\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n"); // Group warnings by category let mut categories = std::collections::HashMap::new(); for warning in &self.warnings { categories .entry(warning.category.clone()) .or_insert_with(Vec::new) .push(warning); } for (category, warnings) in categories { _ = writeln!( report, "\n {} ({} warnings):", category, warnings.len() ); for warning in warnings.iter().take(3) { // Show first 3 warnings per category if let Some(idx) = warning.bar_index { _ = writeln!(report, " [Bar {}] {}", idx, warning.message); } else { _ = writeln!(report, " {}", warning.message); } } if warnings.len() > 3 { _ = writeln!(report, " ... and {} more", warnings.len() - 3); } } report.push('\n'); } report.push_str("\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\n"); report } } /// Validation metrics for Prometheus #[derive(Debug, Clone, Default)] pub struct ValidationMetrics { /// Total validations performed pub total_validations: usize, /// Total bars validated pub total_bars_validated: usize, /// Total errors detected pub total_errors: usize, /// Total warnings detected pub total_warnings: usize, /// Total corrections applied pub total_corrections: usize, } /// Data validator with composable rules pub struct DataValidator { /// Validation rules rules: Vec>, /// Metrics tracking metrics_enabled: bool, metrics: ValidationMetrics, /// Atomic counters for thread-safe metrics validation_counter: AtomicUsize, bars_counter: AtomicUsize, error_counter: AtomicUsize, warning_counter: AtomicUsize, } impl std::fmt::Debug for DataValidator { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("DataValidator") .field( "rules", &format_args!("<{} validation rules>", self.rules.len()), ) .field("metrics_enabled", &self.metrics_enabled) .field("metrics", &self.metrics) .field( "validation_counter", &self.validation_counter.load(Ordering::Relaxed), ) .field("bars_counter", &self.bars_counter.load(Ordering::Relaxed)) .field("error_counter", &self.error_counter.load(Ordering::Relaxed)) .field( "warning_counter", &self.warning_counter.load(Ordering::Relaxed), ) .finish() } } impl DataValidator { /// Create new validator with no rules pub fn new() -> Self { Self { rules: Vec::new(), metrics_enabled: false, metrics: ValidationMetrics::default(), validation_counter: AtomicUsize::new(0), bars_counter: AtomicUsize::new(0), error_counter: AtomicUsize::new(0), warning_counter: AtomicUsize::new(0), } } /// Add validation rule pub fn with_rule(mut self, rule: Box) -> Self { self.rules.push(rule); self } /// Enable metrics tracking pub const fn with_metrics_enabled(mut self, enabled: bool) -> Self { self.metrics_enabled = enabled; self } /// Validate OHLCV bars pub fn validate(&self, bars: &[OHLCVBar]) -> Result { let mut all_errors = Vec::new(); let mut all_warnings = Vec::new(); // Run all rules for rule in &self.rules { let rule_errors = rule.validate_bars(bars)?; for error in rule_errors { match error.severity { Severity::Error => all_errors.push(error), Severity::Warning => all_warnings.push(error), } } } // Update metrics if self.metrics_enabled { self.validation_counter.fetch_add(1, Ordering::Relaxed); self.bars_counter.fetch_add(bars.len(), Ordering::Relaxed); self.error_counter .fetch_add(all_errors.len(), Ordering::Relaxed); self.warning_counter .fetch_add(all_warnings.len(), Ordering::Relaxed); } Ok(ValidationResult::new(all_errors, all_warnings, bars.len())) } /// Validate technical indicators pub fn validate_indicators(&self, indicators: &Indicators) -> Result { let mut all_errors = Vec::new(); let mut all_warnings = Vec::new(); // Run all rules that support indicator validation for rule in &self.rules { let rule_errors = rule.validate_indicators(indicators)?; for error in rule_errors { match error.severity { Severity::Error => all_errors.push(error), Severity::Warning => all_warnings.push(error), } } } // Update metrics if self.metrics_enabled { self.validation_counter.fetch_add(1, Ordering::Relaxed); self.error_counter .fetch_add(all_errors.len(), Ordering::Relaxed); self.warning_counter .fetch_add(all_warnings.len(), Ordering::Relaxed); } Ok(ValidationResult::new( all_errors, all_warnings, indicators.rsi.len(), )) } /// Get validation metrics pub fn get_metrics(&self) -> ValidationMetrics { ValidationMetrics { total_validations: self.validation_counter.load(Ordering::Relaxed), total_bars_validated: self.bars_counter.load(Ordering::Relaxed), total_errors: self.error_counter.load(Ordering::Relaxed), total_warnings: self.warning_counter.load(Ordering::Relaxed), total_corrections: 0, // Updated by corrector } } /// Reset metrics pub fn reset_metrics(&self) { self.validation_counter.store(0, Ordering::Relaxed); self.bars_counter.store(0, Ordering::Relaxed); self.error_counter.store(0, Ordering::Relaxed); self.warning_counter.store(0, Ordering::Relaxed); } } impl Default for DataValidator { fn default() -> Self { Self::new() } } /// Validation report for external consumption #[derive(Debug, Clone)] pub struct ValidationReport { /// Overall status pub status: ValidationStatus, /// Total bars validated pub total_bars: usize, /// Error count pub error_count: usize, /// Warning count pub warning_count: usize, /// Detailed errors pub errors: Vec, /// Detailed warnings pub warnings: Vec, } /// Validation status #[derive(Debug, Clone, PartialEq)] pub enum ValidationStatus { /// All checks passed Pass, /// Some checks failed Fail, /// Warnings present but no errors PassWithWarnings, } impl From for ValidationReport { fn from(result: ValidationResult) -> Self { let status = if result.is_valid() { if result.warnings.is_empty() { ValidationStatus::Pass } else { ValidationStatus::PassWithWarnings } } else { ValidationStatus::Fail }; let errors: Vec = result .errors .iter() .map(|e| format!("{}: {}", e.category, e.message)) .collect(); let warnings: Vec = result .warnings .iter() .map(|w| format!("{}: {}", w.category, w.message)) .collect(); Self { status, total_bars: result.total_bars, error_count: result.error_count(), warning_count: result.warning_count(), errors, warnings, } } } #[cfg(test)] mod tests { use super::*; use crate::rules::IntegrityRule; #[test] fn test_validator_creation() { let validator = DataValidator::new(); assert_eq!(validator.rules.len(), 0); } #[test] fn test_validator_with_rules() { let validator = DataValidator::new().with_rule(Box::new(IntegrityRule::new())); assert_eq!(validator.rules.len(), 1); } #[test] fn test_validation_result_valid() { let result = ValidationResult::new(Vec::new(), Vec::new(), 100); assert!(result.is_valid()); assert_eq!(result.error_count(), 0); assert_eq!(result.warning_count(), 0); } #[test] fn test_validation_result_invalid() { let errors = vec![ValidationError::error("test", "test error")]; let result = ValidationResult::new(errors, Vec::new(), 100); assert!(!result.is_valid()); assert_eq!(result.error_count(), 1); } #[test] fn test_report_generation() { let errors = vec![ValidationError::error("integrity", "test error").at_index(5)]; let warnings = vec![ValidationError::warning("continuity", "test warning").at_index(10)]; let result = ValidationResult::new(errors, warnings, 100); let report = result.generate_report(); assert!(report.contains("FAIL")); assert!(report.contains("integrity")); assert!(report.contains("continuity")); assert!(report.contains("Bar 5")); assert!(report.contains("Bar 10")); } }