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>
433 lines
16 KiB
Rust
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"));
|
|
}
|
|
}
|