Files
foxhunt/crates/ml-data-validation/src/validator.rs
jgrusewski 7ef92983f9 fix(clippy): apply cargo clippy --fix across workspace
Mechanical auto-fixes: redundant borrows, clone on Copy, or_insert_with,
single-char push_str, get(0) → first(), needless borrow, let_and_return.
150 files, no behavior changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:17:51 +01:00

433 lines
16 KiB
Rust

//! # 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<ValidationError>,
/// Validation warnings (potential issues)
pub warnings: Vec<ValidationError>,
/// Total bars validated
pub total_bars: usize,
/// Validation passed (no errors)
valid: bool,
}
impl ValidationResult {
/// Create new validation result
pub fn new(
errors: Vec<ValidationError>,
warnings: Vec<ValidationError>,
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<Box<dyn ValidationRule>>,
/// 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<dyn ValidationRule>) -> 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<ValidationResult> {
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<ValidationResult> {
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<String>,
/// Detailed warnings
pub warnings: Vec<String>,
}
/// 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<ValidationResult> 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<String> = result
.errors
.iter()
.map(|e| format!("{}: {}", e.category, e.message))
.collect();
let warnings: Vec<String> = 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"));
}
}