use crate::quotes::decimal::{Decimal, ZERO, ONE, from_usize};
use crate::quotes::Quote;
#[derive(Debug, Clone, PartialEq)]
pub enum ValidationError {
MissingData { field: String, index: usize },
InvalidPrice { index: usize, price: Decimal },
OhlcInconsistency { index: usize, reason: String },
TimestampOutOfOrder { index: usize },
DuplicateTimestamp { index: usize, timestamp: i64 },
DataGap { start_index: usize, gap_seconds: i64 },
Anomaly { index: usize, reason: String },
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ValidationError::MissingData { field, index } => {
write!(f, "Missing data for field '{}' at index {}", field, index)
}
ValidationError::InvalidPrice { index, price } => {
write!(f, "Invalid price {} at index {}", price, index)
}
ValidationError::OhlcInconsistency { index, reason } => {
write!(f, "OHLC inconsistency at index {}: {}", index, reason)
}
ValidationError::TimestampOutOfOrder { index } => {
write!(f, "Timestamp out of order at index {}", index)
}
ValidationError::DuplicateTimestamp { index, timestamp } => {
write!(f, "Duplicate timestamp {} at index {}", timestamp, index)
}
ValidationError::DataGap {
start_index,
gap_seconds,
} => {
write!(
f,
"Data gap of {} seconds starting at index {}",
gap_seconds, start_index
)
}
ValidationError::Anomaly { index, reason } => {
write!(f, "Anomaly detected at index {}: {}", index, reason)
}
}
}
}
impl std::error::Error for ValidationError {}
#[derive(Debug)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<ValidationError>,
pub warnings: Vec<String>,
}
impl ValidationResult {
pub fn success() -> Self {
Self {
valid: true,
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn failure(errors: Vec<ValidationError>) -> Self {
Self {
valid: false,
errors,
warnings: Vec::new(),
}
}
pub fn with_warning(mut self, warning: String) -> Self {
self.warnings.push(warning);
self
}
}
pub fn validate_quotes(quotes: &[Quote]) -> ValidationResult {
let mut errors = Vec::new();
let mut warnings = Vec::new();
if quotes.is_empty() {
warnings.push("Empty quote list".to_string());
return ValidationResult {
valid: true,
errors,
warnings,
};
}
for (i, quote) in quotes.iter().enumerate() {
if quote.high < quote.low {
errors.push(ValidationError::OhlcInconsistency {
index: i,
reason: format!("High ({}) < Low ({})", quote.high, quote.low),
});
}
if quote.high < quote.open {
errors.push(ValidationError::OhlcInconsistency {
index: i,
reason: format!("High ({}) < Open ({})", quote.high, quote.open),
});
}
if quote.high < quote.close {
errors.push(ValidationError::OhlcInconsistency {
index: i,
reason: format!("High ({}) < Close ({})", quote.high, quote.close),
});
}
if quote.low > quote.open {
errors.push(ValidationError::OhlcInconsistency {
index: i,
reason: format!("Low ({}) > Open ({})", quote.low, quote.open),
});
}
if quote.low > quote.close {
errors.push(ValidationError::OhlcInconsistency {
index: i,
reason: format!("Low ({}) > Close ({})", quote.low, quote.close),
});
}
if quote.open <= Decimal::ZERO {
errors.push(ValidationError::InvalidPrice {
index: i,
price: quote.open,
});
}
if quote.high <= Decimal::ZERO {
errors.push(ValidationError::InvalidPrice {
index: i,
price: quote.high,
});
}
if quote.low <= Decimal::ZERO {
errors.push(ValidationError::InvalidPrice {
index: i,
price: quote.low,
});
}
if quote.close <= Decimal::ZERO {
errors.push(ValidationError::InvalidPrice {
index: i,
price: quote.close,
});
}
if i > 0 {
let prev_timestamp = quotes[i - 1].timestamp;
if quote.timestamp < prev_timestamp {
errors.push(ValidationError::TimestampOutOfOrder { index: i });
} else if quote.timestamp == prev_timestamp {
errors.push(ValidationError::DuplicateTimestamp {
index: i,
timestamp: quote.timestamp,
});
}
}
}
let valid = errors.is_empty();
ValidationResult {
valid,
errors,
warnings,
}
}
pub fn detect_anomalies(quotes: &[Quote], threshold: f64) -> Vec<ValidationError> {
if quotes.len() < 4 {
return Vec::new(); }
let mut errors = Vec::new();
let mut prices: Vec<f64> = quotes.iter().map(|q| q.close).collect();
prices.sort_by(|a: &f64, b: &f64| a.partial_cmp(b).unwrap());
let q1_idx = prices.len() / 4;
let q3_idx = (prices.len() * 3) / 4;
let q1 = prices[q1_idx];
let q3 = prices[q3_idx];
let iqr = q3 - q1;
let lower_bound = q1 - threshold * iqr;
let upper_bound = q3 + threshold * iqr;
for (i, quote) in quotes.iter().enumerate() {
let price = quote.close;
if price < lower_bound {
errors.push(ValidationError::Anomaly {
index: i,
reason: format!(
"Price {} below lower bound {:.2}",
quote.close, lower_bound
),
});
} else if price > upper_bound {
errors.push(ValidationError::Anomaly {
index: i,
reason: format!(
"Price {} above upper bound {:.2}",
quote.close, upper_bound
),
});
}
if i > 0 {
let avg_volume = quotes.iter().map(|q| q.volume as f64).sum::<f64>()
/ quotes.len() as f64;
if quote.volume as f64 > avg_volume * 10.0 {
errors.push(ValidationError::Anomaly {
index: i,
reason: format!("Volume {} is 10x average volume", quote.volume),
});
}
}
}
errors
}
pub fn detect_gaps(quotes: &[Quote], max_gap_seconds: i64) -> Vec<ValidationError> {
let mut errors = Vec::new();
for i in 1..quotes.len() {
let gap = quotes[i].timestamp - quotes[i - 1].timestamp;
if gap > max_gap_seconds {
errors.push(ValidationError::DataGap {
start_index: i - 1,
gap_seconds: gap,
});
}
}
errors
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FillMethod {
Forward,
Backward,
Linear,
Zero,
}
pub fn fill_gaps(quotes: &[Quote], interval_seconds: i64, method: FillMethod) -> Vec<Quote> {
if quotes.is_empty() {
return Vec::new();
}
let mut result = Vec::new();
result.push(quotes[0].clone());
for i in 1..quotes.len() {
let prev = "es[i - 1];
let curr = "es[i];
let gap = curr.timestamp - prev.timestamp;
if gap > interval_seconds {
let num_missing = ((gap / interval_seconds) - 1) as usize;
for j in 1..=num_missing {
let ts = prev.timestamp + (j as i64 * interval_seconds);
let filled_quote = match method {
FillMethod::Forward => Quote {
timestamp: ts,
open: prev.close,
high: prev.close,
low: prev.close,
close: prev.close,
volume: 0,
adjclose: prev.adjclose,
},
FillMethod::Backward => Quote {
timestamp: ts,
open: curr.open,
high: curr.open,
low: curr.open,
close: curr.open,
volume: 0,
adjclose: curr.adjclose,
},
FillMethod::Linear => {
let ratio = Decimal::from(j) / Decimal::from(num_missing + 1);
let interpolated = prev.close + (curr.close - prev.close) * ratio;
Quote {
timestamp: ts,
open: interpolated,
high: interpolated,
low: interpolated,
close: interpolated,
volume: 0,
adjclose: interpolated,
}
}
FillMethod::Zero => Quote {
timestamp: ts,
open: Decimal::ZERO,
high: Decimal::ZERO,
low: Decimal::ZERO,
close: Decimal::ZERO,
volume: 0,
adjclose: Decimal::ZERO,
},
};
result.push(filled_quote);
}
}
result.push(curr.clone());
}
result
}
#[cfg(test)]
mod tests {
use super::*;
fn create_valid_quote(timestamp: i64, close: i64) -> Quote {
Quote {
timestamp,
open: Decimal::new(close, 0),
high: Decimal::new(close + 5, 0),
low: Decimal::new(close - 5, 0),
close: Decimal::new(close, 0),
volume: 1000,
adjclose: Decimal::new(close, 0),
}
}
#[test]
fn test_validate_quotes_success() {
let quotes = vec![create_valid_quote(1000, 100), create_valid_quote(2000, 105)];
let result = validate_quotes("es);
assert!(result.valid);
assert_eq!(result.errors.len(), 0);
}
#[test]
fn test_validate_quotes_ohlc_inconsistency() {
let mut quote = create_valid_quote(1000, 100);
quote.high = Decimal::new(90, 0);
let result = validate_quotes(&[quote]);
assert!(!result.valid);
assert!(result.errors.len() > 0);
}
#[test]
fn test_validate_quotes_negative_price() {
let mut quote = create_valid_quote(1000, 100);
quote.close = Decimal::new(-10, 0);
let result = validate_quotes(&[quote]);
assert!(!result.valid);
assert!(result.errors.iter().any(|e| matches!(
e,
ValidationError::InvalidPrice { .. }
)));
}
#[test]
fn test_validate_quotes_out_of_order() {
let quotes = vec![create_valid_quote(2000, 100), create_valid_quote(1000, 105)];
let result = validate_quotes("es);
assert!(!result.valid);
assert!(result.errors.iter().any(|e| matches!(
e,
ValidationError::TimestampOutOfOrder { .. }
)));
}
#[test]
fn test_validate_quotes_duplicate_timestamp() {
let quotes = vec![create_valid_quote(1000, 100), create_valid_quote(1000, 105)];
let result = validate_quotes("es);
assert!(!result.valid);
assert!(result.errors.iter().any(|e| matches!(
e,
ValidationError::DuplicateTimestamp { .. }
)));
}
#[test]
fn test_detect_gaps() {
let quotes = vec![create_valid_quote(1000, 100), create_valid_quote(5000, 105)];
let gaps = detect_gaps("es, 1000);
assert_eq!(gaps.len(), 1);
}
#[test]
fn test_fill_gaps_forward() {
let quotes = vec![create_valid_quote(1000, 100), create_valid_quote(3000, 110)];
let filled = fill_gaps("es, 1000, FillMethod::Forward);
assert_eq!(filled.len(), 3); assert_eq!(filled[1].timestamp, 2000);
assert_eq!(filled[1].close, Decimal::new(100, 0)); }
#[test]
fn test_fill_gaps_linear() {
let quotes = vec![create_valid_quote(1000, 100), create_valid_quote(3000, 110)];
let filled = fill_gaps("es, 1000, FillMethod::Linear);
assert_eq!(filled.len(), 3);
assert_eq!(filled[1].timestamp, 2000);
assert_eq!(filled[1].close, Decimal::new(105, 0)); }
#[test]
fn test_detect_anomalies() {
let quotes = vec![
create_valid_quote(1000, 100),
create_valid_quote(2000, 102),
create_valid_quote(3000, 101),
create_valid_quote(4000, 103),
create_valid_quote(5000, 500), ];
let anomalies = detect_anomalies("es, 1.5);
assert!(anomalies.len() > 0);
}
}