1use 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#[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 #[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 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 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Default)]
215pub struct ValidationContext {
216 pub eligible_parties: HashSet<String>,
218 pub eligible_securities: HashSet<String>,
220 pub position_limits: HashMap<String, u64>,
222 pub current_date: u64,
224}
225
226#[derive(Debug, Clone)]
228pub struct ValidationStats {
229 pub total_trades: u64,
231 pub valid_trades: u64,
233 pub invalid_trades: u64,
235 pub validation_rate: f64,
237 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, 1700000000,
254 1700172800, )
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; 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; 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 assert!(result.is_valid);
450 }
451}