//! # Validation Rules //! //! Individual validation rules for different aspects of data quality. //! Each rule implements the `ValidationRule` trait and can be composed //! into a comprehensive validation pipeline. use crate::Indicators; use crate::OHLCVBar; use anyhow::Result; /// Validation error information #[derive(Debug, Clone)] pub struct ValidationError { /// Error category pub category: String, /// Error message pub message: String, /// Bar index where error occurred pub bar_index: Option, /// Severity (Error or Warning) pub severity: Severity, } /// Error severity level #[derive(Debug, Clone, PartialEq)] pub enum Severity { /// Critical error that indicates data corruption Error, /// Warning about potential data quality issues Warning, } impl ValidationError { /// Create new error pub fn error, M: Into>(category: C, message: M) -> Self { Self { category: category.into(), message: message.into(), bar_index: None, severity: Severity::Error, } } /// Create new warning pub fn warning, M: Into>(category: C, message: M) -> Self { Self { category: category.into(), message: message.into(), bar_index: None, severity: Severity::Warning, } } /// Set bar index pub const fn at_index(mut self, index: usize) -> Self { self.bar_index = Some(index); self } } /// Validation rule trait /// /// Each rule implements specific validation logic for data quality checks. pub trait ValidationRule: Send + Sync { /// Validate OHLCV bars fn validate_bars(&self, bars: &[OHLCVBar]) -> Result>; /// Validate technical indicators (optional) fn validate_indicators(&self, _indicators: &Indicators) -> Result> { Ok(Vec::new()) // Default: no indicator validation } /// Rule name for reporting fn name(&self) -> &str; } // ============================================================================ // Rule 1: OHLCV Integrity Rule // ============================================================================ /// OHLCV integrity validation /// /// Checks: /// - high >= low /// - high >= open, close /// - low <= open, close /// - volume >= 0 #[derive(Debug)] pub struct IntegrityRule; impl Default for IntegrityRule { fn default() -> Self { Self::new() } } impl IntegrityRule { pub const fn new() -> Self { Self } } impl ValidationRule for IntegrityRule { fn name(&self) -> &str { "OHLCV Integrity" } fn validate_bars(&self, bars: &[OHLCVBar]) -> Result> { let mut errors = Vec::new(); for (i, bar) in bars.iter().enumerate() { // Check: high >= low if bar.high < bar.low { errors.push( ValidationError::error( "integrity", format!("Bar {}: high < low ({:.2} < {:.2})", i, bar.high, bar.low), ) .at_index(i), ); } // Check: high >= open, close if bar.high < bar.open { errors.push( ValidationError::error( "integrity", format!("Bar {}: high < open ({:.2} < {:.2})", i, bar.high, bar.open), ) .at_index(i), ); } if bar.high < bar.close { errors.push( ValidationError::error( "integrity", format!( "Bar {}: high < close ({:.2} < {:.2})", i, bar.high, bar.close ), ) .at_index(i), ); } // Check: low <= open, close if bar.low > bar.open { errors.push( ValidationError::error( "integrity", format!("Bar {}: low > open ({:.2} > {:.2})", i, bar.low, bar.open), ) .at_index(i), ); } if bar.low > bar.close { errors.push( ValidationError::error( "integrity", format!("Bar {}: low > close ({:.2} > {:.2})", i, bar.low, bar.close), ) .at_index(i), ); } // Check: volume >= 0 if bar.volume < 0.0 { errors.push( ValidationError::error( "integrity", format!("Bar {}: negative volume ({:.2})", i, bar.volume), ) .at_index(i), ); } } Ok(errors) } } // ============================================================================ // Rule 2: Price Continuity Rule // ============================================================================ /// Price continuity validation /// /// Detects price spikes (large percentage changes between consecutive bars). /// Default threshold: 20% change #[derive(Debug)] pub struct ContinuityRule { /// Maximum allowed percentage change (0.20 = 20%) threshold: f64, } impl ContinuityRule { pub const fn new(threshold: f64) -> Self { Self { threshold } } } impl ValidationRule for ContinuityRule { fn name(&self) -> &str { "Price Continuity" } fn validate_bars(&self, bars: &[OHLCVBar]) -> Result> { let mut errors = Vec::new(); for i in 1..bars.len() { let prev_close = bars[i - 1].close; let curr_open = bars[i].open; // Calculate percentage change let pct_change = ((curr_open - prev_close) / prev_close).abs(); if pct_change > self.threshold { errors.push( ValidationError::error( "continuity", format!( "Bar {}: price spike of {:.1}% (threshold: {:.1}%)", i, pct_change * 100.0, self.threshold * 100.0 ), ) .at_index(i), ); } } Ok(errors) } } // ============================================================================ // Rule 3: Indicator Validation Rule // ============================================================================ /// Technical indicator validation /// /// Checks: /// - RSI in range [0, 100] /// - No NaN or Infinite values /// - Bollinger bands properly ordered (upper > middle > lower) #[derive(Debug)] pub struct IndicatorRule; impl Default for IndicatorRule { fn default() -> Self { Self::new() } } impl IndicatorRule { pub const fn new() -> Self { Self } } impl ValidationRule for IndicatorRule { fn name(&self) -> &str { "Technical Indicators" } fn validate_bars(&self, _bars: &[OHLCVBar]) -> Result> { // This rule validates indicators, not bars Ok(Vec::new()) } fn validate_indicators(&self, indicators: &Indicators) -> Result> { let mut errors = Vec::new(); // Validate RSI range (0-100) for (i, &rsi) in indicators.rsi.iter().enumerate() { if rsi.is_nan() { errors.push( ValidationError::error("indicator", format!("RSI at index {}: NaN value", i)) .at_index(i), ); } else if rsi.is_infinite() { errors.push( ValidationError::error( "indicator", format!("RSI at index {}: infinite value", i), ) .at_index(i), ); } else if !(0.0..=100.0).contains(&rsi) { errors.push( ValidationError::error( "indicator", format!("RSI at index {}: out of range [{:.2}]", i, rsi), ) .at_index(i), ); } else { // RSI value is valid } } // Validate MACD for NaN/Inf for (i, &macd) in indicators.macd.iter().enumerate() { if macd.is_nan() || macd.is_infinite() { errors.push( ValidationError::error( "indicator", format!("MACD at index {}: invalid value", i), ) .at_index(i), ); } } // Validate Bollinger Bands ordering for i in 0..indicators.bb_upper.len() { let upper = indicators.bb_upper[i]; let middle = indicators.bb_middle[i]; let lower = indicators.bb_lower[i]; if upper.is_nan() || middle.is_nan() || lower.is_nan() { errors.push( ValidationError::error( "indicator", format!("Bollinger Bands at index {}: NaN value", i), ) .at_index(i), ); } else if !(upper >= middle && middle >= lower) { errors.push( ValidationError::warning( "indicator", format!( "Bollinger Bands at index {}: improper ordering (upper: {:.2}, middle: {:.2}, lower: {:.2})", i, upper, middle, lower ), ) .at_index(i), ); } else { // Bollinger Bands properly ordered } } // Validate ATR for negative values for (i, &atr) in indicators.atr.iter().enumerate() { if atr.is_nan() || atr.is_infinite() { errors.push( ValidationError::error( "indicator", format!("ATR at index {}: invalid value", i), ) .at_index(i), ); } else if atr < 0.0 { errors.push( ValidationError::error( "indicator", format!("ATR at index {}: negative value ({:.2})", i, atr), ) .at_index(i), ); } else { // ATR value is valid } } Ok(errors) } } // ============================================================================ // Rule 4: Timestamp Validation Rule // ============================================================================ /// Timestamp alignment validation /// /// Checks: /// - Timestamps are properly ordered /// - No large gaps in time series #[derive(Debug)] pub struct TimestampRule { /// Expected interval between bars in seconds expected_interval_secs: i64, } impl TimestampRule { pub const fn new(expected_interval_secs: i64) -> Self { Self { expected_interval_secs, } } } impl ValidationRule for TimestampRule { fn name(&self) -> &str { "Timestamp Alignment" } fn validate_bars(&self, bars: &[OHLCVBar]) -> Result> { let mut errors = Vec::new(); for i in 1..bars.len() { let prev_ts = bars[i - 1].timestamp; let curr_ts = bars[i].timestamp; // Check: timestamps are ordered if curr_ts <= prev_ts { errors.push( ValidationError::error( "timestamp", format!("Bar {}: timestamp not ordered (current <= previous)", i), ) .at_index(i), ); } // Check: no large gaps let gap_secs = (curr_ts - prev_ts).num_seconds(); let max_gap = self.expected_interval_secs * 3; // Allow up to 3x expected interval if gap_secs > max_gap { errors.push( ValidationError::error( "timestamp", format!( "Bar {}: large gap of {}s (expected: {}s)", i, gap_secs, self.expected_interval_secs ), ) .at_index(i), ); } } Ok(errors) } } // ============================================================================ // Rule 5: Data Completeness Rule // ============================================================================ /// Data completeness validation /// /// Checks for missing bars in the time series based on expected interval. #[derive(Debug)] pub struct CompletenessRule { /// Expected interval between bars in seconds expected_interval_secs: i64, /// Minimum completeness ratio (0.0 - 1.0) min_completeness_ratio: f64, } impl CompletenessRule { pub const fn new(expected_interval_secs: i64, min_completeness_ratio: f64) -> Self { Self { expected_interval_secs, min_completeness_ratio, } } } impl ValidationRule for CompletenessRule { fn name(&self) -> &str { "Data Completeness" } fn validate_bars(&self, bars: &[OHLCVBar]) -> Result> { let mut errors = Vec::new(); if bars.len() < 2 { return Ok(errors); } // Calculate expected number of bars (safe: checked bars.len() >= 2) let start_ts = match bars.first() { Some(b) => b.timestamp, None => return Ok(errors), }; let end_ts = match bars.last() { Some(b) => b.timestamp, None => return Ok(errors), }; let total_duration_secs = (end_ts - start_ts).num_seconds(); let expected_bars = (total_duration_secs / self.expected_interval_secs) as usize + 1; let actual_bars = bars.len(); // Calculate completeness ratio let completeness_ratio = actual_bars as f64 / expected_bars as f64; if completeness_ratio < self.min_completeness_ratio { errors.push(ValidationError::error( "completeness", format!( "Data completeness: {:.1}% (expected: \u{2265}{:.1}%) - {}/{} bars present", completeness_ratio * 100.0, self.min_completeness_ratio * 100.0, actual_bars, expected_bars ), )); } else if completeness_ratio < 1.0 { errors.push(ValidationError::warning( "completeness", format!( "Data completeness: {:.1}% - {}/{} bars present", completeness_ratio * 100.0, actual_bars, expected_bars ), )); } else { // Full completeness, no issues } Ok(errors) } }