rustkernel_clearing/
validation.rs

1//! Trade validation kernel.
2//!
3//! This module provides trade validation for clearing:
4//! - Counterparty eligibility checks
5//! - Security eligibility checks
6//! - Settlement date validation
7//! - Position limit checks
8
9use crate::types::{
10    ErrorSeverity, Trade, TradeStatus, ValidationConfig, ValidationError, ValidationResult,
11};
12use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
13use std::collections::{HashMap, HashSet};
14
15// ============================================================================
16// Clearing Validation Kernel
17// ============================================================================
18
19/// Clearing validation kernel.
20///
21/// Validates trades before they enter the clearing process.
22#[derive(Debug, Clone)]
23pub struct ClearingValidation {
24    metadata: KernelMetadata,
25}
26
27impl Default for ClearingValidation {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33impl ClearingValidation {
34    /// Create a new clearing validation kernel.
35    #[must_use]
36    pub fn new() -> Self {
37        Self {
38            metadata: KernelMetadata::batch("clearing/validation", Domain::Clearing)
39                .with_description("Trade validation for clearing")
40                .with_throughput(100_000)
41                .with_latency_us(50.0),
42        }
43    }
44
45    /// Validate a single trade.
46    pub fn validate(
47        trade: &Trade,
48        config: &ValidationConfig,
49        context: &ValidationContext,
50    ) -> ValidationResult {
51        let mut errors = Vec::new();
52        let mut warnings = Vec::new();
53
54        // Check trade status
55        if trade.status != TradeStatus::Pending {
56            errors.push(ValidationError {
57                code: "INVALID_STATUS".to_string(),
58                message: format!("Trade status {:?} not valid for clearing", trade.status),
59                severity: ErrorSeverity::Critical,
60            });
61        }
62
63        // Check quantity
64        if trade.quantity == 0 {
65            errors.push(ValidationError {
66                code: "ZERO_QUANTITY".to_string(),
67                message: "Trade quantity cannot be zero".to_string(),
68                severity: ErrorSeverity::Critical,
69            });
70        }
71
72        // Check price
73        if trade.price <= 0 {
74            errors.push(ValidationError {
75                code: "INVALID_PRICE".to_string(),
76                message: "Trade price must be positive".to_string(),
77                severity: ErrorSeverity::Critical,
78            });
79        }
80
81        // Check counterparty eligibility
82        if config.check_counterparty {
83            if !context.eligible_parties.contains(&trade.buyer_id) {
84                errors.push(ValidationError {
85                    code: "BUYER_NOT_ELIGIBLE".to_string(),
86                    message: format!("Buyer {} not eligible for clearing", trade.buyer_id),
87                    severity: ErrorSeverity::Critical,
88                });
89            }
90            if !context.eligible_parties.contains(&trade.seller_id) {
91                errors.push(ValidationError {
92                    code: "SELLER_NOT_ELIGIBLE".to_string(),
93                    message: format!("Seller {} not eligible for clearing", trade.seller_id),
94                    severity: ErrorSeverity::Critical,
95                });
96            }
97            if trade.buyer_id == trade.seller_id {
98                warnings.push("Buyer and seller are the same party".to_string());
99            }
100        }
101
102        // Check security eligibility
103        if config.check_security && !context.eligible_securities.contains(&trade.security_id) {
104            errors.push(ValidationError {
105                code: "SECURITY_NOT_ELIGIBLE".to_string(),
106                message: format!("Security {} not eligible for clearing", trade.security_id),
107                severity: ErrorSeverity::Critical,
108            });
109        }
110
111        // Check settlement date
112        if config.check_settlement_date {
113            if trade.settlement_date < trade.trade_date {
114                errors.push(ValidationError {
115                    code: "INVALID_SETTLEMENT_DATE".to_string(),
116                    message: "Settlement date cannot be before trade date".to_string(),
117                    severity: ErrorSeverity::Critical,
118                });
119            }
120
121            let days_to_settle = (trade.settlement_date.saturating_sub(trade.trade_date)) / 86400;
122            if days_to_settle < config.min_settlement_days as u64 {
123                errors.push(ValidationError {
124                    code: "SETTLEMENT_TOO_SOON".to_string(),
125                    message: format!(
126                        "Settlement {} days from trade, minimum is {}",
127                        days_to_settle, config.min_settlement_days
128                    ),
129                    severity: ErrorSeverity::Critical,
130                });
131            }
132            if days_to_settle > config.max_settlement_days as u64 {
133                errors.push(ValidationError {
134                    code: "SETTLEMENT_TOO_FAR".to_string(),
135                    message: format!(
136                        "Settlement {} days from trade, maximum is {}",
137                        days_to_settle, config.max_settlement_days
138                    ),
139                    severity: ErrorSeverity::Critical,
140                });
141            }
142        }
143
144        // Check position limits
145        if config.check_limits {
146            if let Some(&limit) = context.position_limits.get(&trade.security_id) {
147                if trade.quantity.unsigned_abs() > limit {
148                    errors.push(ValidationError {
149                        code: "EXCEEDS_POSITION_LIMIT".to_string(),
150                        message: format!(
151                            "Quantity {} exceeds limit {} for security {}",
152                            trade.quantity, limit, trade.security_id
153                        ),
154                        severity: ErrorSeverity::Critical,
155                    });
156                }
157            }
158        }
159
160        ValidationResult {
161            trade_id: trade.id,
162            is_valid: errors.iter().all(|e| e.severity != ErrorSeverity::Critical),
163            errors,
164            warnings,
165        }
166    }
167
168    /// Validate a batch of trades.
169    pub fn validate_batch(
170        trades: &[Trade],
171        config: &ValidationConfig,
172        context: &ValidationContext,
173    ) -> Vec<ValidationResult> {
174        trades
175            .iter()
176            .map(|trade| Self::validate(trade, config, context))
177            .collect()
178    }
179
180    /// Get validation statistics.
181    pub fn get_stats(results: &[ValidationResult]) -> ValidationStats {
182        let total = results.len() as u64;
183        let valid = results.iter().filter(|r| r.is_valid).count() as u64;
184        let invalid = total - valid;
185
186        let mut error_counts: HashMap<String, u64> = HashMap::new();
187        for result in results {
188            for error in &result.errors {
189                *error_counts.entry(error.code.clone()).or_insert(0) += 1;
190            }
191        }
192
193        ValidationStats {
194            total_trades: total,
195            valid_trades: valid,
196            invalid_trades: invalid,
197            validation_rate: if total > 0 {
198                valid as f64 / total as f64
199            } else {
200                0.0
201            },
202            error_counts,
203        }
204    }
205}
206
207impl GpuKernel for ClearingValidation {
208    fn metadata(&self) -> &KernelMetadata {
209        &self.metadata
210    }
211}
212
213/// Validation context with reference data.
214#[derive(Debug, Clone, Default)]
215pub struct ValidationContext {
216    /// Eligible clearing parties.
217    pub eligible_parties: HashSet<String>,
218    /// Eligible securities.
219    pub eligible_securities: HashSet<String>,
220    /// Position limits per security.
221    pub position_limits: HashMap<String, u64>,
222    /// Current date (for date validation).
223    pub current_date: u64,
224}
225
226/// Validation statistics.
227#[derive(Debug, Clone)]
228pub struct ValidationStats {
229    /// Total trades validated.
230    pub total_trades: u64,
231    /// Valid trades.
232    pub valid_trades: u64,
233    /// Invalid trades.
234    pub invalid_trades: u64,
235    /// Validation success rate.
236    pub validation_rate: f64,
237    /// Error counts by code.
238    pub error_counts: HashMap<String, u64>,
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    fn create_valid_trade() -> Trade {
246        Trade::new(
247            1,
248            "AAPL".to_string(),
249            "BUYER001".to_string(),
250            "SELLER001".to_string(),
251            100,
252            15000, // $150.00
253            1700000000,
254            1700172800, // T+2
255        )
256    }
257
258    fn create_context() -> ValidationContext {
259        let mut ctx = ValidationContext::default();
260        ctx.eligible_parties.insert("BUYER001".to_string());
261        ctx.eligible_parties.insert("SELLER001".to_string());
262        ctx.eligible_securities.insert("AAPL".to_string());
263        ctx.position_limits.insert("AAPL".to_string(), 10000);
264        ctx.current_date = 1700000000;
265        ctx
266    }
267
268    #[test]
269    fn test_validation_metadata() {
270        let kernel = ClearingValidation::new();
271        assert_eq!(kernel.metadata().id, "clearing/validation");
272        assert_eq!(kernel.metadata().domain, Domain::Clearing);
273    }
274
275    #[test]
276    fn test_valid_trade() {
277        let trade = create_valid_trade();
278        let config = ValidationConfig::default();
279        let context = create_context();
280
281        let result = ClearingValidation::validate(&trade, &config, &context);
282
283        assert!(result.is_valid);
284        assert!(result.errors.is_empty());
285    }
286
287    #[test]
288    fn test_zero_quantity() {
289        let mut trade = create_valid_trade();
290        trade.quantity = 0;
291
292        let config = ValidationConfig::default();
293        let context = create_context();
294
295        let result = ClearingValidation::validate(&trade, &config, &context);
296
297        assert!(!result.is_valid);
298        assert!(result.errors.iter().any(|e| e.code == "ZERO_QUANTITY"));
299    }
300
301    #[test]
302    fn test_invalid_price() {
303        let mut trade = create_valid_trade();
304        trade.price = -100;
305
306        let config = ValidationConfig::default();
307        let context = create_context();
308
309        let result = ClearingValidation::validate(&trade, &config, &context);
310
311        assert!(!result.is_valid);
312        assert!(result.errors.iter().any(|e| e.code == "INVALID_PRICE"));
313    }
314
315    #[test]
316    fn test_ineligible_buyer() {
317        let mut trade = create_valid_trade();
318        trade.buyer_id = "UNKNOWN".to_string();
319
320        let config = ValidationConfig::default();
321        let context = create_context();
322
323        let result = ClearingValidation::validate(&trade, &config, &context);
324
325        assert!(!result.is_valid);
326        assert!(result.errors.iter().any(|e| e.code == "BUYER_NOT_ELIGIBLE"));
327    }
328
329    #[test]
330    fn test_ineligible_security() {
331        let mut trade = create_valid_trade();
332        trade.security_id = "UNKNOWN".to_string();
333
334        let config = ValidationConfig::default();
335        let context = create_context();
336
337        let result = ClearingValidation::validate(&trade, &config, &context);
338
339        assert!(!result.is_valid);
340        assert!(
341            result
342                .errors
343                .iter()
344                .any(|e| e.code == "SECURITY_NOT_ELIGIBLE")
345        );
346    }
347
348    #[test]
349    fn test_settlement_before_trade() {
350        let mut trade = create_valid_trade();
351        trade.settlement_date = trade.trade_date - 86400; // 1 day before
352
353        let config = ValidationConfig::default();
354        let context = create_context();
355
356        let result = ClearingValidation::validate(&trade, &config, &context);
357
358        assert!(!result.is_valid);
359        assert!(
360            result
361                .errors
362                .iter()
363                .any(|e| e.code == "INVALID_SETTLEMENT_DATE")
364        );
365    }
366
367    #[test]
368    fn test_exceeds_position_limit() {
369        let mut trade = create_valid_trade();
370        trade.quantity = 50000; // Exceeds 10000 limit
371
372        let config = ValidationConfig::default();
373        let context = create_context();
374
375        let result = ClearingValidation::validate(&trade, &config, &context);
376
377        assert!(!result.is_valid);
378        assert!(
379            result
380                .errors
381                .iter()
382                .any(|e| e.code == "EXCEEDS_POSITION_LIMIT")
383        );
384    }
385
386    #[test]
387    fn test_batch_validation() {
388        let trades = vec![create_valid_trade(), {
389            let mut t = create_valid_trade();
390            t.id = 2;
391            t.quantity = 0;
392            t
393        }];
394
395        let config = ValidationConfig::default();
396        let context = create_context();
397
398        let results = ClearingValidation::validate_batch(&trades, &config, &context);
399
400        assert_eq!(results.len(), 2);
401        assert!(results[0].is_valid);
402        assert!(!results[1].is_valid);
403    }
404
405    #[test]
406    fn test_validation_stats() {
407        let trades = vec![
408            create_valid_trade(),
409            {
410                let mut t = create_valid_trade();
411                t.id = 2;
412                t.quantity = 0;
413                t
414            },
415            {
416                let mut t = create_valid_trade();
417                t.id = 3;
418                t
419            },
420        ];
421
422        let config = ValidationConfig::default();
423        let context = create_context();
424
425        let results = ClearingValidation::validate_batch(&trades, &config, &context);
426        let stats = ClearingValidation::get_stats(&results);
427
428        assert_eq!(stats.total_trades, 3);
429        assert_eq!(stats.valid_trades, 2);
430        assert_eq!(stats.invalid_trades, 1);
431        assert!((stats.validation_rate - 0.666).abs() < 0.01);
432    }
433
434    #[test]
435    fn test_skip_counterparty_check() {
436        let mut trade = create_valid_trade();
437        trade.buyer_id = "UNKNOWN".to_string();
438
439        let config = ValidationConfig {
440            check_counterparty: false,
441            ..ValidationConfig::default()
442        };
443
444        let context = create_context();
445
446        let result = ClearingValidation::validate(&trade, &config, &context);
447
448        // Should be valid since counterparty check is disabled
449        assert!(result.is_valid);
450    }
451}